import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core' import type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core' import type Database from '@tauri-apps/plugin-sql' export type TauriSQLiteDatabaseLike = Pick< Database, `execute` | `select` | `close` | `path` > export type TauriSQLiteDriverOptions = { database: TauriSQLiteDatabaseLike } function assertTransactionCallbackHasDriverArg( fn: (transactionDriver: SQLiteDriver) => Promise, ): void { if (fn.length > 0) { return } throw new InvalidPersistedCollectionConfigError( `SQLiteDriver.transaction callback must accept the transaction driver argument`, ) } function isTauriSQLiteDatabaseLike( value: unknown, ): value is TauriSQLiteDatabaseLike { const candidate = value as Partial return ( typeof value === `object` && value !== null && typeof candidate.path === `string` && typeof candidate.execute === `function` && typeof candidate.select === `function` ) } function normalizeQueryRows( rows: unknown, sql: string, ): ReadonlyArray { if (Array.isArray(rows)) { return rows as ReadonlyArray } throw new InvalidPersistedCollectionConfigError( `Unsupported Tauri SQL query result shape for SQL "${sql}"`, ) } function convertSqlitePlaceholdersToTauri(sql: string): string { let result = `` let parameterIndex = 1 let inSingleQuote = false let inDoubleQuote = false let inLineComment = false let inBlockComment = false for (let index = 0; index < sql.length; index++) { const currentChar = sql[index] const nextChar = sql[index + 1] if (inLineComment) { result += currentChar if (currentChar === `\n`) { inLineComment = false } continue } if (inBlockComment) { result += currentChar if (currentChar === `*` && nextChar === `/`) { result += `/` index++ inBlockComment = false } continue } if (!inSingleQuote && !inDoubleQuote) { if (currentChar === `-` && nextChar === `-`) { result += `--` index++ inLineComment = true continue } if (currentChar === `/` && nextChar === `*`) { result += `/*` index++ inBlockComment = true continue } } if (currentChar === `'` && !inDoubleQuote) { result += currentChar if (inSingleQuote && nextChar === `'`) { result += `'` index++ continue } inSingleQuote = !inSingleQuote continue } if (currentChar === `"` && !inSingleQuote) { result += currentChar if (inDoubleQuote && nextChar === `"`) { result += `"` index++ continue } inDoubleQuote = !inDoubleQuote continue } if (currentChar === `?` && !inSingleQuote && !inDoubleQuote) { result += `$${String(parameterIndex)}` parameterIndex++ continue } result += currentChar } return result } export class TauriSQLiteDriver implements SQLiteDriver { private readonly database: TauriSQLiteDatabaseLike private queue: Promise = Promise.resolve() private nextSavepointId = 1 constructor(options: TauriSQLiteDriverOptions) { if (!isTauriSQLiteDatabaseLike(options.database)) { throw new InvalidPersistedCollectionConfigError( `Tauri SQLite database object must provide execute/select methods`, ) } this.database = options.database } async exec(sql: string): Promise { await this.enqueue(async () => { await this.executeStatement(sql) }) } async query( sql: string, params: ReadonlyArray = [], ): Promise> { return this.enqueue(async () => { const rows = await this.database.select>( convertSqlitePlaceholdersToTauri(sql), params.length > 0 ? [...params] : undefined, ) return normalizeQueryRows(rows, sql) }) } async run(sql: string, params: ReadonlyArray = []): Promise { await this.enqueue(async () => { await this.executeStatement(sql, params) }) } async transaction( fn: (transactionDriver: SQLiteDriver) => Promise, ): Promise { assertTransactionCallbackHasDriverArg(fn) return this.transactionWithDriver(fn) } async transactionWithDriver( fn: (transactionDriver: SQLiteDriver) => Promise, ): Promise { return this.enqueue(async () => { await this.executeStatement(`BEGIN IMMEDIATE`) const transactionDriver = this.createTransactionDriver() try { const result = await fn(transactionDriver) await this.executeStatement(`COMMIT`) return result } catch (error) { try { await this.executeStatement(`ROLLBACK`) } catch { // Keep the original transaction error as the primary failure. } throw error } }) } async close(): Promise { if (typeof this.database.close !== `function`) { return } await Promise.resolve(this.database.close(this.database.path)) } getDatabase(): TauriSQLiteDatabaseLike { return this.database } private async executeStatement( sql: string, params: ReadonlyArray = [], ): Promise { await this.database.execute( convertSqlitePlaceholdersToTauri(sql), params.length > 0 ? [...params] : undefined, ) } private enqueue( operation: () => Promise, ): Promise { const queuedOperation = this.queue.then(operation, operation) this.queue = queuedOperation.then( () => undefined, () => undefined, ) return queuedOperation } private createTransactionDriver(): SQLiteDriver { const transactionDriver: SQLiteDriver = { exec: async (sql) => { await this.executeStatement(sql) }, query: async ( sql: string, params: ReadonlyArray = [], ): Promise> => { const rows = await this.database.select>( convertSqlitePlaceholdersToTauri(sql), params.length > 0 ? [...params] : undefined, ) return normalizeQueryRows(rows, sql) }, run: async (sql, params = []) => { await this.executeStatement(sql, params) }, transaction: async ( fn: (transactionDriver: SQLiteDriver) => Promise, ): Promise => { assertTransactionCallbackHasDriverArg(fn) return this.runNestedTransaction(transactionDriver, fn) }, transactionWithDriver: async ( fn: (transactionDriver: SQLiteDriver) => Promise, ): Promise => this.runNestedTransaction(transactionDriver, fn), } return transactionDriver } private async runNestedTransaction( transactionDriver: SQLiteDriver, fn: (transactionDriver: SQLiteDriver) => Promise, ): Promise { const savepointName = `tsdb_sp_${this.nextSavepointId}` this.nextSavepointId++ await this.executeStatement(`SAVEPOINT ${savepointName}`) try { const result = await fn(transactionDriver) await this.executeStatement(`RELEASE SAVEPOINT ${savepointName}`) return result } catch (error) { await this.executeStatement(`ROLLBACK TO SAVEPOINT ${savepointName}`) await this.executeStatement(`RELEASE SAVEPOINT ${savepointName}`) throw error } } } export function createTauriSQLiteDriver( options: TauriSQLiteDriverOptions, ): TauriSQLiteDriver { return new TauriSQLiteDriver(options) }