Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-2706] fix: Fix issue with SQLite transactions #5934

Merged
merged 4 commits into from
Nov 4, 2024

Conversation

SatishGandham
Copy link
Contributor

@SatishGandham SatishGandham commented Oct 30, 2024

  • Fix transaction within transaction issue
  • Close DB handles on reload
  • Fix GET_ISSUES measurement

Summary by CodeRabbit

  • New Features

    • Introduced a method to ensure proper closure of database connections before unloading the application.
    • Enhanced performance tracking and error handling during issue synchronization and data loading processes.
  • Bug Fixes

    • Corrected a typographical error in variable naming related to workspace members.
  • Refactor

    • Simplified transaction initiation process in workspace data loading.
    • Improved database opening logic for read/write access and creation.
    • Removed unnecessary performance monitoring span in the issue retrieval process.

- Close DB handles on reload
- Fix GET_ISSUES tracking
Copy link
Contributor

coderabbitai bot commented Oct 30, 2024

Walkthrough

The changes involve several modifications to the Storage class in web/core/local-db/storage.sqlite.ts, including the addition of a closeDBConnection method and updates to existing methods for improved error handling and performance tracking. Additionally, the addIssuesBulk function in load-issues.ts now includes an early return for empty issue arrays. The transaction initiation command in load-workspace.ts has been simplified, and a typographical error was corrected. Lastly, the getIssues method in issue.service.ts was simplified by removing a performance monitoring span.

Changes

File Path Change Summary
web/core/local-db/storage.sqlite.ts Added closeDBConnection method; updated reset, clearStorage, initialize, and getIssues methods for error handling and performance tracking.
web/core/local-db/utils/load-issues.ts Modified addIssuesBulk to return early if the issues array is empty.
web/core/local-db/utils/load-workspace.ts Changed SQL command from "BEGIN TRANSACTION;" to "BEGIN;" and corrected a typographical error in memebers.
web/core/services/issue/issue.service.ts Simplified getIssues method by removing the Sentry span for performance monitoring.

Possibly related PRs

Suggested labels

🐛bug, 🧹chore

Suggested reviewers

  • rahulramesha
  • prateekshourya29

🐇 In the realm of code, we hop and play,
With methods refined, we clear the way.
Connections closed, no leaks in sight,
Error handling strong, our code takes flight!
So let’s celebrate with a joyful cheer,
For every change brings us closer, my dear! 🐰✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (6)
web/core/local-db/utils/load-issues.ts (3)

Line range hint 52-57: Fix nested transactions in updateIssue

The updateIssue function creates nested transactions by calling deleteIssueFromLocal and addIssue, which each have their own transaction blocks. This can lead to unexpected behavior with SQLite's transaction handling.

Consider refactoring to use a single transaction:

 export const updateIssue = async (issue: TIssue & { is_local_update: number }) => {
   if (document.hidden || !rootStore.user.localDBEnabled || !persistence.db) return;
 
   const issue_id = issue.id;
-  // delete the issue and its meta data
-  await deleteIssueFromLocal(issue_id);
-  await addIssue(issue);
+  await persistence.db.exec("BEGIN;");
+  try {
+    const deleteQuery = `DELETE from issues where id='${issue_id}'`;
+    const deleteMetaQuery = `delete from issue_meta where issue_id='${issue_id}'`;
+    await persistence.db.exec(deleteQuery);
+    await persistence.db.exec(deleteMetaQuery);
+    await stageIssueInserts(issue);
+    await persistence.db.exec("COMMIT;");
+  } catch (error) {
+    await persistence.db.exec("ROLLBACK;");
+    throw error;
+  }
 };

Line range hint 13-17: Add error handling to transaction blocks

Multiple functions use transactions without proper error handling, which could leave the database in an inconsistent state if an error occurs.

Consider wrapping all transaction blocks with try-catch and ROLLBACK. Example pattern:

 export const addIssuesBulk = async (issues: any, batchSize = 100) => {
   if (!rootStore.user.localDBEnabled || !persistence.db) return;
   if (!issues.length) return;
   const insertStart = performance.now();
-  await persistence.db.exec("BEGIN;");
-
-  for (let i = 0; i < issues.length; i += batchSize) {
-    const batch = issues.slice(i, i + batchSize);
-    // ... processing
-  }
-  await persistence.db.exec("COMMIT;");
+  try {
+    await persistence.db.exec("BEGIN;");
+    for (let i = 0; i < issues.length; i += batchSize) {
+      const batch = issues.slice(i, i + batchSize);
+      // ... processing
+    }
+    await persistence.db.exec("COMMIT;");
+  } catch (error) {
+    await persistence.db.exec("ROLLBACK;");
+    throw error;
+  }
 };

Apply similar error handling to addIssue, deleteIssueFromLocal, and stageIssueInserts functions.

Also applies to: 23-36, 42-49, 89-127


SQL Injection vulnerabilities found in multiple locations

Several instances of unsafe SQL query construction using string interpolation were identified:

  • In load-issues.ts:

    • const query = 'INSERT OR REPLACE INTO issues (${columns}) VALUES (${values});'
    • DELETE from issue_meta where issue_id='${issue_id}'
    • DELETE from issues where id='${issue_id}'
    • delete from issue_meta where issue_id='${issue_id}'
  • In storage.sqlite.ts:

    • DELETE FROM options where key='${key}'
  • In load-workspace.ts:

    • INSERT OR REPLACE INTO ${table} (${columns}) VALUES (${values});

While some queries in the codebase use proper parameterization with bind parameters (e.g., the issue_meta insertions), many critical queries are vulnerable to SQL injection. The code should be updated to consistently use parameterized queries with bind parameters throughout.

Example fix for the issues table insert:

const placeholders = Array(Object.values(sanitizedIssue).length).fill('?').join(',');
const query = `INSERT OR REPLACE INTO issues (${columns}) VALUES (${placeholders});`;
await persistence.db.exec({
  sql: query,
  bind: Object.values(sanitizedIssue)
});
🔗 Analysis chain

Line range hint 89-127: Verify SQL injection protection in stageIssueInserts

The function constructs SQL queries by directly interpolating values, which could be vulnerable to SQL injection.

Let's check for other instances of unsafe SQL construction:

Consider using parameterized queries consistently:

-  const query = `INSERT OR REPLACE INTO issues (${columns}) VALUES (${values});`;
-  await persistence.db.exec(query);
+  const query = `INSERT OR REPLACE INTO issues (${columns}) VALUES (${placeholders});`;
+  await persistence.db.exec({
+    sql: query,
+    bind: Object.values(sanitizedIssue)
+  });
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for direct string interpolation in SQL queries
rg -g '*.ts' "INSERT.*\$\{|DELETE.*\$\{|UPDATE.*\$\{|SELECT.*\$\{"

Length of output: 64


Script:

#!/bin/bash
# Let's try a different approach to find SQL queries with string interpolation
# Search for SQL keywords followed by any string interpolation
rg -g '*.ts' -A 2 -B 2 '`.*(?:INSERT|DELETE|UPDATE|SELECT).*\$\{.*`|`.*\$\{.*(?:INSERT|DELETE|UPDATE|SELECT).*`'

# Also search specifically in the load-issues.ts file
cat "web/core/local-db/utils/load-issues.ts"

Length of output: 7774

web/core/local-db/utils/load-workspace.ts (2)

Line range hint 117-124: Fix typo in variable name

There's a typographical error in the variable name "memebers" which should be "members".

Apply this fix:

- const [labels, modules, cycles, states, estimates, memebers] = await Promise.all(promises);
+ const [labels, modules, cycles, states, estimates, members] = await Promise.all(promises);
  await persistence.db.exec("BEGIN;");
  await batchInserts(labels, "labels", labelSchema);
  await batchInserts(modules, "modules", moduleSchema);
  await batchInserts(cycles, "cycles", cycleSchema);
  await batchInserts(states, "states", stateSchema);
  await batchInserts(estimates, "estimate_points", estimatePointSchema);
- await batchInserts(memebers, "members", memberSchema);
+ await batchInserts(members, "members", memberSchema);

Line range hint 117-126: Add error handling for transaction

The transaction lacks error handling which could leave the database in an inconsistent state if any batch operation fails.

Consider wrapping the transaction in a try-catch block:

  const [labels, modules, cycles, states, estimates, members] = await Promise.all(promises);
  const start = performance.now();
+ try {
    await persistence.db.exec("BEGIN;");
    await batchInserts(labels, "labels", labelSchema);
    await batchInserts(modules, "modules", moduleSchema);
    await batchInserts(cycles, "cycles", cycleSchema);
    await batchInserts(states, "states", stateSchema);
    await batchInserts(estimates, "estimate_points", estimatePointSchema);
    await batchInserts(members, "members", memberSchema);
    await persistence.db.exec("COMMIT");
+ } catch (error) {
+   await persistence.db.exec("ROLLBACK");
+   throw error;
+ }
  const end = performance.now();
web/core/local-db/storage.sqlite.ts (1)

306-308: Simplify arrow function

The arrow function block can be simplified for better readability.

-      [issuesRaw, count] = await startSpan({ name: "GET_ISSUES" }, async () => {
-        return await Promise.all([runQuery(query), runQuery(countQuery)]);
-      });
+      [issuesRaw, count] = await startSpan({ name: "GET_ISSUES" }, async () => 
+        Promise.all([runQuery(query), runQuery(countQuery)])
+      );
🧰 Tools
🪛 GitHub Check: lint-web

[failure] 306-306:
Unexpected block statement surrounding arrow body; move the returned value immediately after the =>

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 403482f and 2cb0445.

📒 Files selected for processing (4)
  • web/core/local-db/storage.sqlite.ts (2 hunks)
  • web/core/local-db/utils/load-issues.ts (1 hunks)
  • web/core/local-db/utils/load-workspace.ts (1 hunks)
  • web/core/services/issue/issue.service.ts (1 hunks)
🧰 Additional context used
🪛 GitHub Check: lint-web
web/core/local-db/storage.sqlite.ts

[failure] 306-306:
Unexpected block statement surrounding arrow body; move the returned value immediately after the =>

🔇 Additional comments (5)
web/core/local-db/utils/load-issues.ts (1)

20-20: LGTM: Efficient handling of empty arrays

The early return for empty arrays prevents unnecessary transaction overhead.

web/core/local-db/utils/load-workspace.ts (1)

118-118: LGTM: Simplified transaction syntax

The change to use BEGIN; instead of BEGIN TRANSACTION; is correct and follows SQLite's standard syntax.

web/core/services/issue/issue.service.ts (1)

74-75: LGTM! Simplified performance tracking.

The removal of Sentry's startSpan wrapper improves code clarity while maintaining functionality. This change aligns with the PR objective to fix GET_ISSUES measurement.

Let's verify that performance tracking is properly handled in the persistence layer:

✅ Verification successful

Performance tracking properly moved to persistence layer

The removal of Sentry's startSpan from issue.service.ts is justified as the persistence layer (storage.sqlite.ts) now handles performance tracking using the native Performance API, as evidenced by:

  • Performance measurement starts at line const start = performance.now();
  • Performance measurement ends at line const end = performance.now();
  • The tracking is implemented at the database query level, which is more accurate for measuring actual data retrieval time
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify performance tracking in persistence layer
# Expected: Find performance tracking implementation in storage layer

# Check for Sentry span usage in storage implementation
rg -A 5 "getIssues.*startSpan" "web/core/local-db/"

Length of output: 51


Script:

#!/bin/bash
# Let's broaden our search to find the persistence layer implementation
# First, find where getIssues is implemented
ast-grep --pattern 'async getIssues($$$) { $$ }'

# Also check for any performance tracking related imports or usage
rg -l "startSpan|performance|measure" "web/core/"

Length of output: 380


Script:

#!/bin/bash
# Let's check the implementation in storage.sqlite.ts and load-issues.ts which seem most relevant
rg -A 10 "getIssues" "web/core/local-db/storage.sqlite.ts" "web/core/local-db/utils/load-issues.ts"

# Also check for any performance measurement implementation in these files
rg -A 5 "performance\.mark|performance\.measure" "web/core/local-db/storage.sqlite.ts" "web/core/local-db/utils/load-issues.ts"

Length of output: 4000

web/core/local-db/storage.sqlite.ts (2)

Line range hint 306-314: Improved error handling and performance tracking

The changes properly handle errors by falling back to server requests and include comprehensive performance tracking with Sentry spans.

🧰 Tools
🪛 GitHub Check: lint-web

[failure] 306-306:
Unexpected block statement surrounding arrow body; move the returned value immediately after the =>


Line range hint 1-1: Verify transaction management implementation

The PR objective mentions fixing "transaction within transaction" issue, but the transaction management code isn't visible in this file. Please ensure the fix is implemented in the worker code.

Let's verify the transaction implementation:

web/core/local-db/storage.sqlite.ts Show resolved Hide resolved
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (2)
web/core/local-db/worker/db.ts (2)

Line range hint 114-127: Enhance connection cleanup for worker termination scenarios.

While the close() method exists, consider adding automatic cleanup for unexpected worker termination scenarios to prevent resource leaks. This aligns with the PR objective to "Close DB handles on reload".

Add a cleanup handler:

+  constructor() {
+    // Ensure DB is closed when worker is terminated
+    self.addEventListener('beforeunload', async () => {
+      await this.close();
+    });
+  }

Transaction management needs improvement for reliability and error handling

The verification confirms the need for a robust transaction manager. Current implementation shows several issues:

  • No ROLLBACK handling in application code despite transactions being used extensively
  • Complex promise-based queue system in db.ts that only handles "BEGIN;" string literal
  • Multiple transaction blocks in different utilities without proper error handling
  • No nested transaction support despite SQLite's SAVEPOINT capability being available
  • Transactions are used for bulk operations but lack proper cleanup on failure

Key locations requiring attention:

  • web/core/local-db/worker/db.ts: Core transaction handling logic
  • web/core/local-db/utils/load-issues.ts: Multiple transaction blocks without error handling
  • web/core/local-db/utils/load-workspace.ts: Bulk operation transaction without safety nets
  • web/core/local-db/utils/tables.ts: Table creation transaction without rollback capability
🔗 Analysis chain

Line range hint 65-108: Consider implementing a robust transaction manager.

The current transaction handling using a promise queue is complex and fragile. Consider implementing a dedicated transaction manager class that:

  1. Properly tracks transaction states
  2. Handles nested transactions using SQLite's SAVEPOINT
  3. Provides clear transaction boundaries and rollback capabilities

Let's verify the current transaction usage patterns:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for different transaction start patterns that might be incompatible
echo "Checking transaction patterns in the codebase:"
rg -i "begin\s*(transaction)?\s*;" --type ts

# Check for potential nested transactions
echo -e "\nChecking for potential nested transactions:"
ast-grep --pattern 'BEGIN' -l ts | xargs -I {} rg -A 10 'BEGIN' {} | grep -i 'begin'

Length of output: 867


Script:

#!/bin/bash
# Let's analyze the transaction patterns more comprehensively

# Check for COMMIT patterns
echo "Checking COMMIT patterns:"
rg -i "commit\s*;" --type ts

# Check for ROLLBACK patterns
echo -e "\nChecking ROLLBACK patterns:"
rg -i "rollback\s*;" --type ts

# Check the context around BEGIN statements to understand the transaction flow
echo -e "\nAnalyzing transaction flow context:"
for file in $(rg -l "BEGIN;" --type ts); do
    echo -e "\nFile: $file"
    rg -B 5 -A 15 "BEGIN;" "$file"
done

# Check if there are any SAVEPOINT usages
echo -e "\nChecking for SAVEPOINT usage:"
rg -i "savepoint" --type ts

Length of output: 5929

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 85d711f and 51610d1.

📒 Files selected for processing (2)
  • web/core/local-db/utils/load-workspace.ts (1 hunks)
  • web/core/local-db/worker/db.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/core/local-db/utils/load-workspace.ts
🔇 Additional comments (1)
web/core/local-db/worker/db.ts (1)

39-44: LGTM: Proper database initialization parameters.

The explicit parameters for open_v2 are correct and align with best practices:

  • OPEN_READWRITE | OPEN_CREATE ensures proper access mode
  • VFS name "plane" matches the registered virtual file system

Comment on lines +65 to +66
// @todo this will fail if the transaction is started any other way
// eg: BEGIN, OR BEGIN TRANSACTION
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Transaction syntax limitations could cause failures.

The code only handles "BEGIN;" but not other valid SQLite transaction start commands like "BEGIN TRANSACTION;" or "BEGIN". This limitation could lead to transaction deadlocks or failures, especially when transactions are nested.

Consider updating the condition to handle all valid transaction start syntaxes:

-    if (props === "BEGIN;") {
+    if (typeof props === "string" && 
+        (props === "BEGIN;" || 
+         props === "BEGIN" || 
+         props === "BEGIN TRANSACTION;")) {

Committable suggestion skipped: line range outside the PR's diff.

@pushya22 pushya22 merged commit a1bfde6 into preview Nov 4, 2024
14 of 15 checks passed
@pushya22 pushya22 deleted the fix-transaction-errors-sqlite branch November 4, 2024 11:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants