-
Notifications
You must be signed in to change notification settings - Fork 3.6k
[WEB-2706] fix: Fix issue with SQLite transactions #5919
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,13 +40,14 @@ export const addIssuesBulk = async (issues: any, batchSize = 100) => { | |
| export const deleteIssueFromLocal = async (issue_id: any) => { | ||
| if (!rootStore.user.localDBEnabled || !persistence.db) return; | ||
|
|
||
| const deleteQuery = `delete from issues where id='${issue_id}'`; | ||
| const deleteQuery = `DELETE from issues where id='${issue_id}'`; | ||
| const deleteMetaQuery = `delete from issue_meta where issue_id='${issue_id}'`; | ||
|
|
||
| persistence.db.exec("BEGIN;"); | ||
| persistence.db.exec(deleteQuery); | ||
| persistence.db.exec(deleteMetaQuery); | ||
| persistence.db.exec("COMMIT;"); | ||
| await persistence.db.exec("BEGIN;"); | ||
|
|
||
| await persistence.db.exec(deleteQuery); | ||
| await persistence.db.exec(deleteMetaQuery); | ||
| await persistence.db.exec("COMMIT;"); | ||
| }; | ||
| // @todo: Update deletes the issue description from local. Implement a separate update. | ||
| export const updateIssue = async (issue: TIssue & { is_local_update: number }) => { | ||
|
|
@@ -55,7 +56,7 @@ export const updateIssue = async (issue: TIssue & { is_local_update: number }) = | |
| const issue_id = issue.id; | ||
| // delete the issue and its meta data | ||
| await deleteIssueFromLocal(issue_id); | ||
| addIssue(issue); | ||
| await addIssue(issue); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling for the update operation. While the 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);
+ try {
+ await persistence.db.exec("BEGIN;");
+ await deleteIssueFromLocal(issue_id);
+ await addIssue(issue);
+ await persistence.db.exec("COMMIT;");
+ } catch (error) {
+ await persistence.db.exec("ROLLBACK;");
+ throw error;
+ }
};
|
||
| }; | ||
|
|
||
| export const syncDeletesToLocal = async (workspaceId: string, projectId: string, queries: any) => { | ||
|
|
@@ -98,27 +99,33 @@ const stageIssueInserts = async (issue: any) => { | |
| .join(", "); | ||
|
|
||
| const query = `INSERT OR REPLACE INTO issues (${columns}) VALUES (${values});`; | ||
| persistence.db.exec(query); | ||
| await persistence.db.exec(query); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Protect against SQL injection in the main issue insert. The main issue insert query is constructed using string interpolation, making it vulnerable to SQL injection. Consider using parameterized queries here as well: - const query = `INSERT OR REPLACE INTO issues (${columns}) VALUES (${values});`;
- await persistence.db.exec(query);
+ await persistence.db.exec({
+ sql: `INSERT OR REPLACE INTO issues (${columns}) VALUES (${placeholders})`,
+ bind: Object.values(sanitizedIssue)
+ });
|
||
|
|
||
| await persistence.db.exec({ | ||
| sql: `DELETE from issue_meta where issue_id='${issue_id}'`, | ||
| }); | ||
|
|
||
| const metaPromises: Promise<any>[] = []; | ||
|
|
||
| ARRAY_FIELDS.forEach((field) => { | ||
| const values = issue[field]; | ||
| if (values && values.length) { | ||
| values.forEach((val: any) => { | ||
| persistence.db.exec({ | ||
| const p = persistence.db.exec({ | ||
| sql: `INSERT OR REPLACE into issue_meta(issue_id,key,value) values (?,?,?) `, | ||
| bind: [issue_id, field, val], | ||
| }); | ||
| metaPromises.push(p); | ||
| }); | ||
| } else { | ||
| // Added for empty fields? | ||
| persistence.db.exec({ | ||
| const p = persistence.db.exec({ | ||
| sql: `INSERT OR REPLACE into issue_meta(issue_id,key,value) values (?,?,?) `, | ||
| bind: [issue_id, field, ""], | ||
| }); | ||
| metaPromises.push(p); | ||
| } | ||
| }); | ||
|
|
||
| await Promise.all(metaPromises); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -59,7 +59,7 @@ export const updatePersistentLayer = async (issueIds: string | string[]) => { | |||||||||||||||||||||||||||||||||||||||||||||||
| "type_id", | ||||||||||||||||||||||||||||||||||||||||||||||||
| "description_html", | ||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||
| updateIssue({ ...issuePartial, is_local_update: 1 }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| await updateIssue({ ...issuePartial, is_local_update: 1 }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix async operation handling in forEach loop. While adding Refactor the function to properly handle concurrent updates: export const updatePersistentLayer = async (issueIds: string | string[]) => {
if (typeof issueIds === "string") {
issueIds = [issueIds];
}
- issueIds.forEach(async (issueId) => {
+ await Promise.all(issueIds.map(async (issueId) => {
const dbIssue = await persistence.getIssue(issueId);
const issue = rootStore.issue.issues.getIssueById(issueId);
if (issue) {
const issuePartial = pick({ ...dbIssue, ...JSON.parse(JSON.stringify(issue)) }, [
// ... fields ...
]);
- await updateIssue({ ...issuePartial, is_local_update: 1 });
+ try {
+ await updateIssue({ ...issuePartial, is_local_update: 1 });
+ } catch (error) {
+ logError(error);
+ throw error; // Re-throw to ensure Promise.all catches the failure
+ }
}
- });
+ }));
};This change:
📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,8 +24,8 @@ interface SQLiteInstance { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class DBClass { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private instance: SQLiteInstance = {} as SQLiteInstance; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private sqlite3: any; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private tp: Promise<any> | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private tpResolver: any; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private tp: Promise<any>[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private tpResolver: any = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+28
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use more descriptive variable names and explicit types for clarity The variables Consider renaming and typing the variables to improve readability and maintainability: - private tp: Promise<any>[] = [];
- private tpResolver: any = [];
+ private transactionPromises: Promise<void>[] = [];
+ private transactionResolvers: Array<{ resolve: Function; reject: Function }> = [];📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async init(dbName: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!dbName || typeof dbName !== "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Invalid database name"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -57,8 +57,19 @@ export class DBClass { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async exec(props: string | TQueryProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.tp && props === "BEGIN;") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await this.tp; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (props === "BEGIN;") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let promiseToAwait; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.tp.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| promiseToAwait = this.tp.shift(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const p = new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.tpResolver.push({ resolve, reject }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.tp.push(p); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (promiseToAwait) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await promiseToAwait; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+60
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Refactor transaction management to prevent potential misalignment issues Managing transactions using separate arrays for promises and resolvers ( Refactor the transaction management to use a unified structure that pairs each promise with its resolver. This enhances code clarity and reduces the risk of synchronization issues. Apply the following changes: - if (props === "BEGIN;") {
- let promiseToAwait;
- if (this.tp.length > 0) {
- promiseToAwait = this.tp.shift();
- }
- const p = new Promise((resolve, reject) => {
- this.tpResolver.push({ resolve, reject });
- });
- this.tp.push(p);
-
- if (promiseToAwait) {
- await promiseToAwait;
- }
- }
+ if (props === "BEGIN;") {
+ let previousTransaction;
+ if (this.transactionQueue.length > 0) {
+ previousTransaction = this.transactionQueue[this.transactionQueue.length - 1].promise;
+ }
+ let resolveFunction;
+ const promise = new Promise<void>((resolve, reject) => {
+ resolveFunction = resolve;
+ });
+ this.transactionQueue.push({ promise, resolve: resolveFunction });
+
+ if (previousTransaction) {
+ await previousTransaction;
+ }
+ }📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let sql: string, bind: any[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof props === "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -84,16 +95,12 @@ export class DBClass { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (sql === "BEGIN;") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.tp = new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.tpResolver = { resolve, reject }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (sql === "COMMIT;" && this.tp) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await this.instance.exec(sql); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.tpResolver.resolve(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.tp = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.tp.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { resolve } = this.tpResolver.shift(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await this.instance.exec(sql); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add SQL injection protection and error handling.
The current implementation has two security/reliability concerns:
Consider refactoring to use parameterized queries and add error handling:
export const deleteIssueFromLocal = async (issue_id: any) => { if (!rootStore.user.localDBEnabled || !persistence.db) return; - const deleteQuery = `DELETE from issues where id='${issue_id}'`; - const deleteMetaQuery = `delete from issue_meta where issue_id='${issue_id}'`; + try { + await persistence.db.exec("BEGIN;"); - await persistence.db.exec("BEGIN;"); + await persistence.db.exec({ + sql: "DELETE FROM issues WHERE id = ?", + bind: [issue_id] + }); + await persistence.db.exec({ + sql: "DELETE FROM issue_meta WHERE issue_id = ?", + bind: [issue_id] + }); - await persistence.db.exec(deleteQuery); - await persistence.db.exec(deleteMetaQuery); - await persistence.db.exec("COMMIT;"); + await persistence.db.exec("COMMIT;"); + } catch (error) { + await persistence.db.exec("ROLLBACK;"); + throw error; + } };📝 Committable suggestion