Skip to content

Commit

Permalink
fix (client): extend wa-sqlite's critical section to avoid bad interl…
Browse files Browse the repository at this point in the history
…eavings (#688)

This PR extends the critical section of our `exec` method for wa-sqlite which fixes #682. The mutex now wraps all wa-sqlite calls to avoid any bad interleavings.
  • Loading branch information
kevin-dp authored Nov 23, 2023
1 parent db9c2e2 commit 863f9f3
Show file tree
Hide file tree
Showing 2 changed files with 18 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-rules-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"electric-sql": patch
---

Fix critical section of wa-sqlite DB adapter to avoid bad interleavings.
34 changes: 13 additions & 21 deletions clients/typescript/src/drivers/wa-sqlite/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,23 @@ export class ElectricDatabase {
async exec(statement: Statement): Promise<Row[]> {
// Uses a mutex to ensure that the execution of SQL statements is not interleaved
// otherwise wa-sqlite may encounter problems such as indices going out of bounds
// all calls to wa-sqlite need to be coordinated through this mutex
const release = await this.#mutex.acquire()

const str = this.sqlite3.str_new(this.db, statement.sql)
let prepared
try {
prepared = await this.sqlite3.prepare_v2(
this.db,
this.sqlite3.str_value(str)
)
// Need to wrap all sqlite statements in a try..finally block
// that releases the lock at the very end, even if an error occurs
return await this.execSql(statement)
} finally {
release()
}
}

if (prepared === null) {
release()
return []
}

const stmt = prepared.stmt
try {
// Calls to this method must always be coordinated through the mutex
private async execSql(statement: Statement): Promise<Row[]> {
// `statements` is a convenience function that manages statement compilation
// such that we don't have to prepare and finalize statements ourselves
// cf. https://rhashimoto.github.io/wa-sqlite/docs/interfaces/SQLiteAPI.html#statements
for await (const stmt of this.sqlite3.statements(this.db, statement.sql)) {
if (typeof statement.args !== 'undefined') {
this.sqlite3.bind_collection(
stmt,
Expand All @@ -65,25 +62,20 @@ export class ElectricDatabase {
| SQLiteCompatibleType[]
)
}

const rows: SqlValue[][] = []
let cols: string[] = []

while ((await this.sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {
cols = cols.length === 0 ? this.sqlite3.column_names(stmt) : cols
const row = this.sqlite3.row(stmt) as SqlValue[]
rows.push(row)
}

const res = {
columns: cols,
values: rows,
}
return resultToRows(res)
} finally {
await this.sqlite3.finalize(stmt)
release()
return resultToRows(res) // exit loop after one statement
}
return [] // will get here only if there is no statement
}

getRowsModified() {
Expand Down

0 comments on commit 863f9f3

Please sign in to comment.