@tanstack/tauri-db-sqlite-persistence 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,290 @@
1
+ import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'
2
+ import type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core'
3
+ import type Database from '@tauri-apps/plugin-sql'
4
+
5
+ export type TauriSQLiteDatabaseLike = Pick<
6
+ Database,
7
+ `execute` | `select` | `close` | `path`
8
+ >
9
+
10
+ export type TauriSQLiteDriverOptions = {
11
+ database: TauriSQLiteDatabaseLike
12
+ }
13
+
14
+ function assertTransactionCallbackHasDriverArg(
15
+ fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,
16
+ ): void {
17
+ if (fn.length > 0) {
18
+ return
19
+ }
20
+
21
+ throw new InvalidPersistedCollectionConfigError(
22
+ `SQLiteDriver.transaction callback must accept the transaction driver argument`,
23
+ )
24
+ }
25
+
26
+ function isTauriSQLiteDatabaseLike(
27
+ value: unknown,
28
+ ): value is TauriSQLiteDatabaseLike {
29
+ const candidate = value as Partial<TauriSQLiteDatabaseLike>
30
+ return (
31
+ typeof value === `object` &&
32
+ value !== null &&
33
+ typeof candidate.path === `string` &&
34
+ typeof candidate.execute === `function` &&
35
+ typeof candidate.select === `function`
36
+ )
37
+ }
38
+
39
+ function normalizeQueryRows<TRow>(
40
+ rows: unknown,
41
+ sql: string,
42
+ ): ReadonlyArray<TRow> {
43
+ if (Array.isArray(rows)) {
44
+ return rows as ReadonlyArray<TRow>
45
+ }
46
+
47
+ throw new InvalidPersistedCollectionConfigError(
48
+ `Unsupported Tauri SQL query result shape for SQL "${sql}"`,
49
+ )
50
+ }
51
+
52
+ function convertSqlitePlaceholdersToTauri(sql: string): string {
53
+ let result = ``
54
+ let parameterIndex = 1
55
+ let inSingleQuote = false
56
+ let inDoubleQuote = false
57
+ let inLineComment = false
58
+ let inBlockComment = false
59
+
60
+ for (let index = 0; index < sql.length; index++) {
61
+ const currentChar = sql[index]
62
+ const nextChar = sql[index + 1]
63
+
64
+ if (inLineComment) {
65
+ result += currentChar
66
+ if (currentChar === `\n`) {
67
+ inLineComment = false
68
+ }
69
+ continue
70
+ }
71
+
72
+ if (inBlockComment) {
73
+ result += currentChar
74
+ if (currentChar === `*` && nextChar === `/`) {
75
+ result += `/`
76
+ index++
77
+ inBlockComment = false
78
+ }
79
+ continue
80
+ }
81
+
82
+ if (!inSingleQuote && !inDoubleQuote) {
83
+ if (currentChar === `-` && nextChar === `-`) {
84
+ result += `--`
85
+ index++
86
+ inLineComment = true
87
+ continue
88
+ }
89
+
90
+ if (currentChar === `/` && nextChar === `*`) {
91
+ result += `/*`
92
+ index++
93
+ inBlockComment = true
94
+ continue
95
+ }
96
+ }
97
+
98
+ if (currentChar === `'` && !inDoubleQuote) {
99
+ result += currentChar
100
+ if (inSingleQuote && nextChar === `'`) {
101
+ result += `'`
102
+ index++
103
+ continue
104
+ }
105
+ inSingleQuote = !inSingleQuote
106
+ continue
107
+ }
108
+
109
+ if (currentChar === `"` && !inSingleQuote) {
110
+ result += currentChar
111
+ if (inDoubleQuote && nextChar === `"`) {
112
+ result += `"`
113
+ index++
114
+ continue
115
+ }
116
+ inDoubleQuote = !inDoubleQuote
117
+ continue
118
+ }
119
+
120
+ if (currentChar === `?` && !inSingleQuote && !inDoubleQuote) {
121
+ result += `$${String(parameterIndex)}`
122
+ parameterIndex++
123
+ continue
124
+ }
125
+
126
+ result += currentChar
127
+ }
128
+
129
+ return result
130
+ }
131
+
132
+ export class TauriSQLiteDriver implements SQLiteDriver {
133
+ private readonly database: TauriSQLiteDatabaseLike
134
+ private queue: Promise<void> = Promise.resolve()
135
+ private nextSavepointId = 1
136
+
137
+ constructor(options: TauriSQLiteDriverOptions) {
138
+ if (!isTauriSQLiteDatabaseLike(options.database)) {
139
+ throw new InvalidPersistedCollectionConfigError(
140
+ `Tauri SQLite database object must provide execute/select methods`,
141
+ )
142
+ }
143
+
144
+ this.database = options.database
145
+ }
146
+
147
+ async exec(sql: string): Promise<void> {
148
+ await this.enqueue(async () => {
149
+ await this.executeStatement(sql)
150
+ })
151
+ }
152
+
153
+ async query<TRow>(
154
+ sql: string,
155
+ params: ReadonlyArray<unknown> = [],
156
+ ): Promise<ReadonlyArray<TRow>> {
157
+ return this.enqueue(async () => {
158
+ const rows = await this.database.select<ReadonlyArray<TRow>>(
159
+ convertSqlitePlaceholdersToTauri(sql),
160
+ params.length > 0 ? [...params] : undefined,
161
+ )
162
+ return normalizeQueryRows<TRow>(rows, sql)
163
+ })
164
+ }
165
+
166
+ async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {
167
+ await this.enqueue(async () => {
168
+ await this.executeStatement(sql, params)
169
+ })
170
+ }
171
+
172
+ async transaction<TReturn>(
173
+ fn: (transactionDriver: SQLiteDriver) => Promise<TReturn>,
174
+ ): Promise<TReturn> {
175
+ assertTransactionCallbackHasDriverArg(fn)
176
+ return this.transactionWithDriver(fn)
177
+ }
178
+
179
+ async transactionWithDriver<TReturn>(
180
+ fn: (transactionDriver: SQLiteDriver) => Promise<TReturn>,
181
+ ): Promise<TReturn> {
182
+ return this.enqueue(async () => {
183
+ await this.executeStatement(`BEGIN IMMEDIATE`)
184
+ const transactionDriver = this.createTransactionDriver()
185
+
186
+ try {
187
+ const result = await fn(transactionDriver)
188
+ await this.executeStatement(`COMMIT`)
189
+ return result
190
+ } catch (error) {
191
+ try {
192
+ await this.executeStatement(`ROLLBACK`)
193
+ } catch {
194
+ // Keep the original transaction error as the primary failure.
195
+ }
196
+ throw error
197
+ }
198
+ })
199
+ }
200
+
201
+ async close(): Promise<void> {
202
+ if (typeof this.database.close !== `function`) {
203
+ return
204
+ }
205
+
206
+ await Promise.resolve(this.database.close(this.database.path))
207
+ }
208
+
209
+ getDatabase(): TauriSQLiteDatabaseLike {
210
+ return this.database
211
+ }
212
+
213
+ private async executeStatement(
214
+ sql: string,
215
+ params: ReadonlyArray<unknown> = [],
216
+ ): Promise<void> {
217
+ await this.database.execute(
218
+ convertSqlitePlaceholdersToTauri(sql),
219
+ params.length > 0 ? [...params] : undefined,
220
+ )
221
+ }
222
+
223
+ private enqueue<TReturn>(
224
+ operation: () => Promise<TReturn>,
225
+ ): Promise<TReturn> {
226
+ const queuedOperation = this.queue.then(operation, operation)
227
+ this.queue = queuedOperation.then(
228
+ () => undefined,
229
+ () => undefined,
230
+ )
231
+ return queuedOperation
232
+ }
233
+
234
+ private createTransactionDriver(): SQLiteDriver {
235
+ const transactionDriver: SQLiteDriver = {
236
+ exec: async (sql) => {
237
+ await this.executeStatement(sql)
238
+ },
239
+ query: async <TRow>(
240
+ sql: string,
241
+ params: ReadonlyArray<unknown> = [],
242
+ ): Promise<ReadonlyArray<TRow>> => {
243
+ const rows = await this.database.select<ReadonlyArray<TRow>>(
244
+ convertSqlitePlaceholdersToTauri(sql),
245
+ params.length > 0 ? [...params] : undefined,
246
+ )
247
+ return normalizeQueryRows<TRow>(rows, sql)
248
+ },
249
+ run: async (sql, params = []) => {
250
+ await this.executeStatement(sql, params)
251
+ },
252
+ transaction: async <TReturn>(
253
+ fn: (transactionDriver: SQLiteDriver) => Promise<TReturn>,
254
+ ): Promise<TReturn> => {
255
+ assertTransactionCallbackHasDriverArg(fn)
256
+ return this.runNestedTransaction(transactionDriver, fn)
257
+ },
258
+ transactionWithDriver: async <TReturn>(
259
+ fn: (transactionDriver: SQLiteDriver) => Promise<TReturn>,
260
+ ): Promise<TReturn> => this.runNestedTransaction(transactionDriver, fn),
261
+ }
262
+
263
+ return transactionDriver
264
+ }
265
+
266
+ private async runNestedTransaction<TReturn>(
267
+ transactionDriver: SQLiteDriver,
268
+ fn: (transactionDriver: SQLiteDriver) => Promise<TReturn>,
269
+ ): Promise<TReturn> {
270
+ const savepointName = `tsdb_sp_${this.nextSavepointId}`
271
+ this.nextSavepointId++
272
+ await this.executeStatement(`SAVEPOINT ${savepointName}`)
273
+
274
+ try {
275
+ const result = await fn(transactionDriver)
276
+ await this.executeStatement(`RELEASE SAVEPOINT ${savepointName}`)
277
+ return result
278
+ } catch (error) {
279
+ await this.executeStatement(`ROLLBACK TO SAVEPOINT ${savepointName}`)
280
+ await this.executeStatement(`RELEASE SAVEPOINT ${savepointName}`)
281
+ throw error
282
+ }
283
+ }
284
+ }
285
+
286
+ export function createTauriSQLiteDriver(
287
+ options: TauriSQLiteDriverOptions,
288
+ ): TauriSQLiteDriver {
289
+ return new TauriSQLiteDriver(options)
290
+ }
package/src/tauri.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { createTauriSQLitePersistence as createPersistence } from './tauri-persistence'
2
+ import type {
3
+ TauriSQLitePersistenceOptions as TauriSQLitePersistenceOptionsBase,
4
+ TauriSQLiteSchemaMismatchPolicy as TauriSQLiteSchemaMismatchPolicyBase,
5
+ } from './tauri-persistence'
6
+ import type { PersistedCollectionPersistence } from '@tanstack/db-sqlite-persistence-core'
7
+
8
+ export type TauriSQLitePersistenceOptions = TauriSQLitePersistenceOptionsBase
9
+ export type TauriSQLiteSchemaMismatchPolicy =
10
+ TauriSQLiteSchemaMismatchPolicyBase
11
+ export type { TauriSQLiteDatabaseLike } from './tauri-persistence'
12
+
13
+ export function createTauriSQLitePersistence<
14
+ T extends object,
15
+ TKey extends string | number = string | number,
16
+ >(
17
+ options: TauriSQLitePersistenceOptions,
18
+ ): PersistedCollectionPersistence<T, TKey> {
19
+ return createPersistence<T, TKey>(options)
20
+ }