@anfenn/dync 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/capacitor.cjs +228 -0
- package/dist/capacitor.cjs.map +1 -0
- package/dist/capacitor.d.cts +62 -0
- package/dist/capacitor.d.ts +62 -0
- package/dist/capacitor.js +9 -0
- package/dist/capacitor.js.map +1 -0
- package/dist/chunk-LGHOZECP.js +3884 -0
- package/dist/chunk-LGHOZECP.js.map +1 -0
- package/dist/chunk-SQB6E7V2.js +191 -0
- package/dist/chunk-SQB6E7V2.js.map +1 -0
- package/dist/dexie-Bv-fV10P.d.cts +444 -0
- package/dist/dexie-DJFApKsM.d.ts +444 -0
- package/dist/dexie.cjs +381 -0
- package/dist/dexie.cjs.map +1 -0
- package/dist/dexie.d.cts +3 -0
- package/dist/dexie.d.ts +3 -0
- package/dist/dexie.js +343 -0
- package/dist/dexie.js.map +1 -0
- package/dist/expoSqlite.cjs +98 -0
- package/dist/expoSqlite.cjs.map +1 -0
- package/dist/expoSqlite.d.cts +17 -0
- package/dist/expoSqlite.d.ts +17 -0
- package/dist/expoSqlite.js +61 -0
- package/dist/expoSqlite.js.map +1 -0
- package/dist/index.cjs +3916 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/index.shared-CPIge2ZM.d.ts +234 -0
- package/dist/index.shared-YSn6c01d.d.cts +234 -0
- package/dist/node.cjs +126 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +80 -0
- package/dist/node.d.ts +80 -0
- package/dist/node.js +89 -0
- package/dist/node.js.map +1 -0
- package/dist/react/index.cjs +1754 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +40 -0
- package/dist/react/index.d.ts +40 -0
- package/dist/react/index.js +78 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types-CSbIAfu2.d.cts +46 -0
- package/dist/types-CSbIAfu2.d.ts +46 -0
- package/dist/wa-sqlite.cjs +318 -0
- package/dist/wa-sqlite.cjs.map +1 -0
- package/dist/wa-sqlite.d.cts +175 -0
- package/dist/wa-sqlite.d.ts +175 -0
- package/dist/wa-sqlite.js +281 -0
- package/dist/wa-sqlite.js.map +1 -0
- package/package.json +171 -0
- package/src/addVisibilityChangeListener.native.ts +33 -0
- package/src/addVisibilityChangeListener.ts +24 -0
- package/src/capacitor.ts +4 -0
- package/src/core/StateManager.ts +272 -0
- package/src/core/firstLoad.ts +332 -0
- package/src/core/pullOperations.ts +212 -0
- package/src/core/pushOperations.ts +290 -0
- package/src/core/tableEnhancers.ts +457 -0
- package/src/core/types.ts +3 -0
- package/src/createLocalId.native.ts +8 -0
- package/src/createLocalId.ts +6 -0
- package/src/dexie.ts +2 -0
- package/src/expoSqlite.ts +2 -0
- package/src/helpers.ts +87 -0
- package/src/index.native.ts +28 -0
- package/src/index.shared.ts +613 -0
- package/src/index.ts +28 -0
- package/src/logger.ts +26 -0
- package/src/node.ts +4 -0
- package/src/react/index.ts +2 -0
- package/src/react/useDync.ts +156 -0
- package/src/storage/dexie/DexieAdapter.ts +72 -0
- package/src/storage/dexie/DexieQueryContext.ts +14 -0
- package/src/storage/dexie/DexieStorageCollection.ts +124 -0
- package/src/storage/dexie/DexieStorageTable.ts +123 -0
- package/src/storage/dexie/DexieStorageWhereClause.ts +103 -0
- package/src/storage/dexie/helpers.ts +1 -0
- package/src/storage/dexie/index.ts +7 -0
- package/src/storage/memory/MemoryAdapter.ts +55 -0
- package/src/storage/memory/MemoryCollection.ts +215 -0
- package/src/storage/memory/MemoryQueryContext.ts +14 -0
- package/src/storage/memory/MemoryTable.ts +336 -0
- package/src/storage/memory/MemoryWhereClause.ts +134 -0
- package/src/storage/memory/index.ts +7 -0
- package/src/storage/memory/types.ts +24 -0
- package/src/storage/sqlite/SQLiteAdapter.ts +564 -0
- package/src/storage/sqlite/SQLiteCollection.ts +294 -0
- package/src/storage/sqlite/SQLiteTable.ts +604 -0
- package/src/storage/sqlite/SQLiteWhereClause.ts +341 -0
- package/src/storage/sqlite/SqliteQueryContext.ts +30 -0
- package/src/storage/sqlite/drivers/BetterSqlite3Driver.ts +156 -0
- package/src/storage/sqlite/drivers/CapacitorFastSqlDriver.ts +114 -0
- package/src/storage/sqlite/drivers/CapacitorSQLiteDriver.ts +137 -0
- package/src/storage/sqlite/drivers/ExpoSQLiteDriver.native.ts +67 -0
- package/src/storage/sqlite/drivers/WaSqliteDriver.ts +537 -0
- package/src/storage/sqlite/drivers/wa-sqlite-vfs.d.ts +46 -0
- package/src/storage/sqlite/helpers.ts +144 -0
- package/src/storage/sqlite/index.ts +11 -0
- package/src/storage/sqlite/schema.ts +44 -0
- package/src/storage/sqlite/types.ts +164 -0
- package/src/storage/types.ts +112 -0
- package/src/types.ts +186 -0
- package/src/wa-sqlite.ts +4 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import type { StorageWhereClause, StorageCollection } from '../types';
|
|
2
|
+
import type { SQLiteCondition } from './types';
|
|
3
|
+
import { SQLiteTable } from './SQLiteTable';
|
|
4
|
+
import { SQLiteCollection } from './SQLiteCollection';
|
|
5
|
+
|
|
6
|
+
export class SQLiteWhereClause<T = any> implements StorageWhereClause<T> {
|
|
7
|
+
private readonly table: SQLiteTable<T>;
|
|
8
|
+
private readonly index: string | string[];
|
|
9
|
+
private readonly baseCollection?: SQLiteCollection<T>;
|
|
10
|
+
|
|
11
|
+
constructor(table: SQLiteTable<T>, index: string | string[], baseCollection?: SQLiteCollection<T>) {
|
|
12
|
+
this.table = table;
|
|
13
|
+
this.index = index;
|
|
14
|
+
this.baseCollection = baseCollection;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private getColumn(): string {
|
|
18
|
+
// For compound indexes, we only support the first column in SQL conditions
|
|
19
|
+
// Complex compound index queries fall back to JS
|
|
20
|
+
if (Array.isArray(this.index)) {
|
|
21
|
+
return this.index[0]!;
|
|
22
|
+
}
|
|
23
|
+
return this.index;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private isCompoundIndex(): boolean {
|
|
27
|
+
return Array.isArray(this.index) && this.index.length > 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private getCompoundColumns(): string[] {
|
|
31
|
+
return Array.isArray(this.index) ? this.index : [this.index];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private getCompoundValues(value: unknown): unknown[] {
|
|
35
|
+
// For compound indexes, value should be an array of values matching each column
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
// Single value for single-column index
|
|
40
|
+
return [value];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private createCollectionWithCondition(condition: SQLiteCondition): SQLiteCollection<T> {
|
|
44
|
+
const base = this.baseCollection ?? this.table.createCollection();
|
|
45
|
+
return base.addSqlCondition(condition);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private createCollectionWithJsPredicate(predicate: (record: T) => boolean): SQLiteCollection<T> {
|
|
49
|
+
const base = this.baseCollection ?? this.table.createCollection();
|
|
50
|
+
return base.jsFilter(predicate) as SQLiteCollection<T>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private getIndexValue(record: T): unknown {
|
|
54
|
+
return this.table.getIndexValue(record, this.index);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private flattenArgs<TValue>(args: any[]): TValue[] {
|
|
58
|
+
if (args.length === 1 && Array.isArray(args[0])) {
|
|
59
|
+
return args[0] as TValue[];
|
|
60
|
+
}
|
|
61
|
+
return args as TValue[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
equals(value: any): StorageCollection<T> {
|
|
65
|
+
if (this.isCompoundIndex()) {
|
|
66
|
+
// Use native SQL: WHERE col1 = ? AND col2 = ? AND ...
|
|
67
|
+
const columns = this.getCompoundColumns();
|
|
68
|
+
const values = this.getCompoundValues(value);
|
|
69
|
+
return this.createCollectionWithCondition({
|
|
70
|
+
type: 'compoundEquals',
|
|
71
|
+
columns,
|
|
72
|
+
values,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return this.createCollectionWithCondition({
|
|
76
|
+
type: 'equals',
|
|
77
|
+
column: this.getColumn(),
|
|
78
|
+
value,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
above(value: any): StorageCollection<T> {
|
|
83
|
+
if (this.isCompoundIndex()) {
|
|
84
|
+
return this.createCollectionWithJsPredicate((record) => this.table.compareValues(this.getIndexValue(record), value) > 0);
|
|
85
|
+
}
|
|
86
|
+
return this.createCollectionWithCondition({
|
|
87
|
+
type: 'comparison',
|
|
88
|
+
column: this.getColumn(),
|
|
89
|
+
op: '>',
|
|
90
|
+
value,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
aboveOrEqual(value: any): StorageCollection<T> {
|
|
95
|
+
if (this.isCompoundIndex()) {
|
|
96
|
+
return this.createCollectionWithJsPredicate((record) => this.table.compareValues(this.getIndexValue(record), value) >= 0);
|
|
97
|
+
}
|
|
98
|
+
return this.createCollectionWithCondition({
|
|
99
|
+
type: 'comparison',
|
|
100
|
+
column: this.getColumn(),
|
|
101
|
+
op: '>=',
|
|
102
|
+
value,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
below(value: any): StorageCollection<T> {
|
|
107
|
+
if (this.isCompoundIndex()) {
|
|
108
|
+
return this.createCollectionWithJsPredicate((record) => this.table.compareValues(this.getIndexValue(record), value) < 0);
|
|
109
|
+
}
|
|
110
|
+
return this.createCollectionWithCondition({
|
|
111
|
+
type: 'comparison',
|
|
112
|
+
column: this.getColumn(),
|
|
113
|
+
op: '<',
|
|
114
|
+
value,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
belowOrEqual(value: any): StorageCollection<T> {
|
|
119
|
+
if (this.isCompoundIndex()) {
|
|
120
|
+
return this.createCollectionWithJsPredicate((record) => this.table.compareValues(this.getIndexValue(record), value) <= 0);
|
|
121
|
+
}
|
|
122
|
+
return this.createCollectionWithCondition({
|
|
123
|
+
type: 'comparison',
|
|
124
|
+
column: this.getColumn(),
|
|
125
|
+
op: '<=',
|
|
126
|
+
value,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
between(lower: any, upper: any, includeLower = true, includeUpper = false): StorageCollection<T> {
|
|
131
|
+
if (this.isCompoundIndex()) {
|
|
132
|
+
return this.createCollectionWithJsPredicate((record) => {
|
|
133
|
+
const value = this.getIndexValue(record);
|
|
134
|
+
const lowerCmp = this.table.compareValues(value, lower);
|
|
135
|
+
const upperCmp = this.table.compareValues(value, upper);
|
|
136
|
+
const lowerPass = includeLower ? lowerCmp >= 0 : lowerCmp > 0;
|
|
137
|
+
const upperPass = includeUpper ? upperCmp <= 0 : upperCmp < 0;
|
|
138
|
+
return lowerPass && upperPass;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return this.createCollectionWithCondition({
|
|
142
|
+
type: 'between',
|
|
143
|
+
column: this.getColumn(),
|
|
144
|
+
lower,
|
|
145
|
+
upper,
|
|
146
|
+
includeLower,
|
|
147
|
+
includeUpper,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
inAnyRange(ranges: Array<[any, any]>, options?: { includeLower?: boolean; includeUpper?: boolean }): StorageCollection<T> {
|
|
152
|
+
// inAnyRange with multiple ranges uses OR logic - can be expressed in SQL
|
|
153
|
+
if (this.isCompoundIndex() || ranges.length === 0) {
|
|
154
|
+
return this.createCollectionWithJsPredicate((record) => {
|
|
155
|
+
const value = this.getIndexValue(record);
|
|
156
|
+
return ranges.some(([lower, upper]) => {
|
|
157
|
+
const lowerCmp = this.table.compareValues(value, lower);
|
|
158
|
+
const upperCmp = this.table.compareValues(value, upper);
|
|
159
|
+
const lowerPass = options?.includeLower !== false ? lowerCmp >= 0 : lowerCmp > 0;
|
|
160
|
+
const upperPass = options?.includeUpper ? upperCmp <= 0 : upperCmp < 0;
|
|
161
|
+
return lowerPass && upperPass;
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const column = this.getColumn();
|
|
167
|
+
const orConditions: SQLiteCondition[] = ranges.map(([lower, upper]) => ({
|
|
168
|
+
type: 'between' as const,
|
|
169
|
+
column,
|
|
170
|
+
lower,
|
|
171
|
+
upper,
|
|
172
|
+
includeLower: options?.includeLower !== false,
|
|
173
|
+
includeUpper: options?.includeUpper ?? false,
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
return this.createCollectionWithCondition({
|
|
177
|
+
type: 'or',
|
|
178
|
+
conditions: orConditions,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
startsWith(prefix: string): StorageCollection<T> {
|
|
183
|
+
if (this.isCompoundIndex()) {
|
|
184
|
+
return this.createCollectionWithJsPredicate((record) => String(this.getIndexValue(record) ?? '').startsWith(prefix));
|
|
185
|
+
}
|
|
186
|
+
// SQLite LIKE pattern: 'prefix%' matches strings starting with prefix
|
|
187
|
+
// Escape special LIKE characters in the prefix
|
|
188
|
+
const escapedPrefix = prefix.replace(/[%_\\]/g, '\\$&');
|
|
189
|
+
return this.createCollectionWithCondition({
|
|
190
|
+
type: 'like',
|
|
191
|
+
column: this.getColumn(),
|
|
192
|
+
pattern: `${escapedPrefix}%`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
startsWithIgnoreCase(prefix: string): StorageCollection<T> {
|
|
197
|
+
if (this.isCompoundIndex()) {
|
|
198
|
+
return this.createCollectionWithJsPredicate((record) =>
|
|
199
|
+
String(this.getIndexValue(record) ?? '')
|
|
200
|
+
.toLowerCase()
|
|
201
|
+
.startsWith(prefix.toLowerCase()),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const escapedPrefix = prefix.replace(/[%_\\]/g, '\\$&');
|
|
205
|
+
return this.createCollectionWithCondition({
|
|
206
|
+
type: 'like',
|
|
207
|
+
column: this.getColumn(),
|
|
208
|
+
pattern: `${escapedPrefix}%`,
|
|
209
|
+
caseInsensitive: true,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
startsWithAnyOf(...args: any[]): StorageCollection<T> {
|
|
214
|
+
const prefixes = this.flattenArgs<string>(args);
|
|
215
|
+
if (this.isCompoundIndex() || prefixes.length === 0) {
|
|
216
|
+
return this.createCollectionWithJsPredicate((record) => {
|
|
217
|
+
const value = String(this.getIndexValue(record) ?? '');
|
|
218
|
+
return prefixes.some((prefix) => value.startsWith(prefix));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const column = this.getColumn();
|
|
223
|
+
const orConditions: SQLiteCondition[] = prefixes.map((prefix) => {
|
|
224
|
+
const escapedPrefix = prefix.replace(/[%_\\]/g, '\\$&');
|
|
225
|
+
return {
|
|
226
|
+
type: 'like' as const,
|
|
227
|
+
column,
|
|
228
|
+
pattern: `${escapedPrefix}%`,
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return this.createCollectionWithCondition({
|
|
233
|
+
type: 'or',
|
|
234
|
+
conditions: orConditions,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
startsWithAnyOfIgnoreCase(...args: any[]): StorageCollection<T> {
|
|
239
|
+
const prefixes = this.flattenArgs<string>(args);
|
|
240
|
+
if (this.isCompoundIndex() || prefixes.length === 0) {
|
|
241
|
+
const lowerPrefixes = prefixes.map((p) => p.toLowerCase());
|
|
242
|
+
return this.createCollectionWithJsPredicate((record) => {
|
|
243
|
+
const value = String(this.getIndexValue(record) ?? '').toLowerCase();
|
|
244
|
+
return lowerPrefixes.some((prefix) => value.startsWith(prefix));
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const column = this.getColumn();
|
|
249
|
+
const orConditions: SQLiteCondition[] = prefixes.map((prefix) => {
|
|
250
|
+
const escapedPrefix = prefix.replace(/[%_\\]/g, '\\$&');
|
|
251
|
+
return {
|
|
252
|
+
type: 'like' as const,
|
|
253
|
+
column,
|
|
254
|
+
pattern: `${escapedPrefix}%`,
|
|
255
|
+
caseInsensitive: true,
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return this.createCollectionWithCondition({
|
|
260
|
+
type: 'or',
|
|
261
|
+
conditions: orConditions,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
equalsIgnoreCase(value: string): StorageCollection<T> {
|
|
266
|
+
if (this.isCompoundIndex()) {
|
|
267
|
+
return this.createCollectionWithJsPredicate((record) => String(this.getIndexValue(record) ?? '').toLowerCase() === value.toLowerCase());
|
|
268
|
+
}
|
|
269
|
+
return this.createCollectionWithCondition({
|
|
270
|
+
type: 'equals',
|
|
271
|
+
column: this.getColumn(),
|
|
272
|
+
value,
|
|
273
|
+
caseInsensitive: true,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
anyOf(...args: any[]): StorageCollection<T> {
|
|
278
|
+
const values = this.flattenArgs<any>(args);
|
|
279
|
+
if (this.isCompoundIndex()) {
|
|
280
|
+
// Use native SQL: (col1 = ? AND col2 = ?) OR (col1 = ? AND col2 = ?) OR ...
|
|
281
|
+
const columns = this.getCompoundColumns();
|
|
282
|
+
const orConditions: SQLiteCondition[] = values.map((value) => ({
|
|
283
|
+
type: 'compoundEquals' as const,
|
|
284
|
+
columns,
|
|
285
|
+
values: this.getCompoundValues(value),
|
|
286
|
+
}));
|
|
287
|
+
return this.createCollectionWithCondition({
|
|
288
|
+
type: 'or',
|
|
289
|
+
conditions: orConditions,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return this.createCollectionWithCondition({
|
|
293
|
+
type: 'in',
|
|
294
|
+
column: this.getColumn(),
|
|
295
|
+
values,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
anyOfIgnoreCase(...args: any[]): StorageCollection<T> {
|
|
300
|
+
const values = this.flattenArgs<string>(args);
|
|
301
|
+
if (this.isCompoundIndex()) {
|
|
302
|
+
const lowerValues = values.map((v) => v.toLowerCase());
|
|
303
|
+
return this.createCollectionWithJsPredicate((record) => {
|
|
304
|
+
const value = String(this.getIndexValue(record) ?? '').toLowerCase();
|
|
305
|
+
return lowerValues.includes(value);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return this.createCollectionWithCondition({
|
|
309
|
+
type: 'in',
|
|
310
|
+
column: this.getColumn(),
|
|
311
|
+
values,
|
|
312
|
+
caseInsensitive: true,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
noneOf(...args: any[]): StorageCollection<T> {
|
|
317
|
+
const values = this.flattenArgs<any>(args);
|
|
318
|
+
if (this.isCompoundIndex()) {
|
|
319
|
+
return this.createCollectionWithJsPredicate((record) =>
|
|
320
|
+
values.every((candidate) => this.table.compareValues(this.getIndexValue(record), candidate) !== 0),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return this.createCollectionWithCondition({
|
|
324
|
+
type: 'notIn',
|
|
325
|
+
column: this.getColumn(),
|
|
326
|
+
values,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
notEqual(value: any): StorageCollection<T> {
|
|
331
|
+
if (this.isCompoundIndex()) {
|
|
332
|
+
return this.createCollectionWithJsPredicate((record) => this.table.compareValues(this.getIndexValue(record), value) !== 0);
|
|
333
|
+
}
|
|
334
|
+
return this.createCollectionWithCondition({
|
|
335
|
+
type: 'comparison',
|
|
336
|
+
column: this.getColumn(),
|
|
337
|
+
op: '!=',
|
|
338
|
+
value,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { StorageTable, StorageTransactionContext, TransactionMode } from '../types';
|
|
2
|
+
import type { SQLiteDatabaseDriver, SQLiteRunResult } from './types';
|
|
3
|
+
import { SQLiteAdapter } from './SQLiteAdapter';
|
|
4
|
+
|
|
5
|
+
export class SqliteQueryContext {
|
|
6
|
+
constructor(
|
|
7
|
+
private readonly driver: SQLiteDatabaseDriver,
|
|
8
|
+
private readonly adapter: SQLiteAdapter,
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
table<T = any>(name: string): StorageTable<T> {
|
|
12
|
+
return this.adapter.table(name);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
transaction<T>(mode: TransactionMode, tableNames: string[], callback: (context: StorageTransactionContext) => Promise<T>): Promise<T> {
|
|
16
|
+
return this.adapter.transaction(mode, tableNames, callback);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async execute(statement: string): Promise<void> {
|
|
20
|
+
return this.driver.execute(statement);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async run(statement: string, values?: any[]): Promise<SQLiteRunResult> {
|
|
24
|
+
return this.driver.run(statement, values);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async queryRows(statement: string, values?: any[]): Promise<Array<Record<string, any>>> {
|
|
28
|
+
return this.adapter.queryRows(statement, values);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { SQLiteDatabaseDriver, SQLiteQueryResult, SQLiteRunResult } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for configuring the BetterSqlite3Driver.
|
|
5
|
+
*/
|
|
6
|
+
export interface BetterSqlite3DriverOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Open the database in readonly mode.
|
|
9
|
+
* @default false
|
|
10
|
+
*/
|
|
11
|
+
readonly?: boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create the database file if it doesn't exist.
|
|
15
|
+
* Set to false to throw an error if the file doesn't exist.
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
fileMustExist?: boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Timeout in milliseconds when waiting for the database to become unlocked.
|
|
22
|
+
* @default 5000
|
|
23
|
+
*/
|
|
24
|
+
timeout?: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Enable verbose mode for debugging SQL statements.
|
|
28
|
+
*/
|
|
29
|
+
verbose?: (message?: unknown, ...additionalArgs: unknown[]) => void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Enable WAL (Write-Ahead Logging) mode for better concurrent access.
|
|
33
|
+
* Recommended for most use cases.
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
36
|
+
wal?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* SQLite driver for Node.js using better-sqlite3.
|
|
41
|
+
* This driver is synchronous but wraps operations in Promises for API compatibility.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { BetterSqlite3Driver } from '@anfenn/dync/node';
|
|
46
|
+
* import { SQLiteAdapter } from '@anfenn/dync';
|
|
47
|
+
*
|
|
48
|
+
* const driver = new BetterSqlite3Driver('myapp.db', { wal: true });
|
|
49
|
+
* const adapter = new SQLiteAdapter('myapp', driver);
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export class BetterSqlite3Driver implements SQLiteDatabaseDriver {
|
|
53
|
+
readonly type = 'BetterSqlite3Driver';
|
|
54
|
+
private db: import('better-sqlite3').Database | null = null;
|
|
55
|
+
private readonly options: BetterSqlite3DriverOptions;
|
|
56
|
+
private opened = false;
|
|
57
|
+
readonly name: string;
|
|
58
|
+
|
|
59
|
+
constructor(databasePath: string, options: BetterSqlite3DriverOptions = {}) {
|
|
60
|
+
this.name = databasePath;
|
|
61
|
+
this.options = {
|
|
62
|
+
wal: true,
|
|
63
|
+
...options,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async open(): Promise<void> {
|
|
68
|
+
if (this.opened) return;
|
|
69
|
+
|
|
70
|
+
// Dynamic import to avoid bundling issues in non-Electron environments
|
|
71
|
+
const Database = (await import('better-sqlite3')).default;
|
|
72
|
+
|
|
73
|
+
this.db = new Database(this.name, {
|
|
74
|
+
readonly: this.options.readonly ?? false,
|
|
75
|
+
fileMustExist: this.options.fileMustExist ?? false,
|
|
76
|
+
timeout: this.options.timeout ?? 5000,
|
|
77
|
+
verbose: this.options.verbose,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Enable WAL mode for better concurrent access (recommended)
|
|
81
|
+
if (this.options.wal && !this.options.readonly) {
|
|
82
|
+
this.db.pragma('journal_mode = WAL');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.opened = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async close(): Promise<void> {
|
|
89
|
+
if (!this.db) return;
|
|
90
|
+
|
|
91
|
+
this.db.close();
|
|
92
|
+
this.db = null;
|
|
93
|
+
this.opened = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async execute(statement: string): Promise<void> {
|
|
97
|
+
await this.open();
|
|
98
|
+
this.db!.exec(statement);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async run(statement: string, values: unknown[] = []): Promise<SQLiteRunResult> {
|
|
102
|
+
await this.open();
|
|
103
|
+
|
|
104
|
+
const stmt = this.db!.prepare(statement);
|
|
105
|
+
const result = stmt.run(...values);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
changes: result.changes,
|
|
109
|
+
lastId: result.lastInsertRowid !== undefined ? Number(result.lastInsertRowid) : undefined,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async query(statement: string, values: unknown[] = []): Promise<SQLiteQueryResult> {
|
|
114
|
+
await this.open();
|
|
115
|
+
|
|
116
|
+
const stmt = this.db!.prepare(statement);
|
|
117
|
+
const rows = stmt.all(...values) as Record<string, unknown>[];
|
|
118
|
+
|
|
119
|
+
if (rows.length === 0) {
|
|
120
|
+
return { columns: [], values: [] };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const firstRow = rows[0]!;
|
|
124
|
+
const columns = Object.keys(firstRow);
|
|
125
|
+
const valuesMatrix = rows.map((row) => columns.map((col) => row[col]));
|
|
126
|
+
|
|
127
|
+
return { columns, values: valuesMatrix };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Access the underlying better-sqlite3 Database instance for advanced operations.
|
|
132
|
+
* Returns null if the database is not open.
|
|
133
|
+
*/
|
|
134
|
+
getDatabase(): import('better-sqlite3').Database | null {
|
|
135
|
+
return this.db;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Execute a function within a transaction.
|
|
140
|
+
* This provides better performance when doing many writes.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* await driver.transaction(() => {
|
|
145
|
+
* driver.run('INSERT INTO users (name) VALUES (?)', ['Alice']);
|
|
146
|
+
* driver.run('INSERT INTO users (name) VALUES (?)', ['Bob']);
|
|
147
|
+
* });
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
transaction<T>(fn: () => T): T {
|
|
151
|
+
if (!this.db) {
|
|
152
|
+
throw new Error('Database not open. Call open() first.');
|
|
153
|
+
}
|
|
154
|
+
return this.db.transaction(fn)();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { SQLConnection, SQLValue } from '@capgo/capacitor-fast-sql';
|
|
2
|
+
|
|
3
|
+
import type { SQLiteDatabaseDriver, SQLiteRunResult, SQLiteQueryResult } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface FastSqlDriverOptions {
|
|
6
|
+
encrypted?: boolean;
|
|
7
|
+
getEncryptionKey?: () => string;
|
|
8
|
+
readonly?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Lazily loaded module cache to avoid top-level imports that break web bundlers
|
|
12
|
+
let fastSqlModuleCache: typeof import('@capgo/capacitor-fast-sql') | null = null;
|
|
13
|
+
|
|
14
|
+
async function getFastSqlModule(): Promise<typeof import('@capgo/capacitor-fast-sql')> {
|
|
15
|
+
if (!fastSqlModuleCache) {
|
|
16
|
+
fastSqlModuleCache = await import('@capgo/capacitor-fast-sql');
|
|
17
|
+
}
|
|
18
|
+
return fastSqlModuleCache;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* SQLiteDatabaseDriver implementation for @capgo/capacitor-fast-sql plugin.
|
|
23
|
+
*
|
|
24
|
+
* This driver provides a compatible interface with the existing SQLiteAdapter
|
|
25
|
+
* while using the Capgo Fast SQL plugin which offers better performance through
|
|
26
|
+
* a local HTTP server approach that bypasses Capacitor's bridge.
|
|
27
|
+
*
|
|
28
|
+
* Update your AndroidManifest.xml to allow cleartext traffic:
|
|
29
|
+
* <application
|
|
30
|
+
...
|
|
31
|
+
android:usesCleartextTraffic="true">
|
|
32
|
+
|
|
33
|
+
Requires Capacitor HTTP plugin to prevent CORS, as FastSql server runs on localhost:9000, not Capacitor's localhost
|
|
34
|
+
*/
|
|
35
|
+
export class CapacitorFastSqlDriver implements SQLiteDatabaseDriver {
|
|
36
|
+
readonly type = 'CapacitorFastSqlDriver';
|
|
37
|
+
private readonly options: FastSqlDriverOptions;
|
|
38
|
+
private db?: SQLConnection;
|
|
39
|
+
private openPromise?: Promise<void>;
|
|
40
|
+
private opened = false;
|
|
41
|
+
readonly name: string;
|
|
42
|
+
|
|
43
|
+
constructor(databaseName: string, options: FastSqlDriverOptions = {}) {
|
|
44
|
+
this.name = databaseName;
|
|
45
|
+
this.options = options;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async open(): Promise<void> {
|
|
49
|
+
if (this.opened) return;
|
|
50
|
+
if (this.openPromise) return this.openPromise;
|
|
51
|
+
|
|
52
|
+
this.openPromise = (async () => {
|
|
53
|
+
if (!this.db) {
|
|
54
|
+
const { FastSQL } = await getFastSqlModule();
|
|
55
|
+
const encryptionKey = this.options.getEncryptionKey?.();
|
|
56
|
+
if (!encryptionKey && this.options.encrypted) {
|
|
57
|
+
throw new Error('FastSqlDriverOptions.encrypted=true but no encryption key was provided (getEncryptionKey).');
|
|
58
|
+
}
|
|
59
|
+
this.db = await FastSQL.connect({
|
|
60
|
+
database: this.name,
|
|
61
|
+
encrypted: this.options.encrypted,
|
|
62
|
+
encryptionKey,
|
|
63
|
+
readOnly: this.options.readonly,
|
|
64
|
+
});
|
|
65
|
+
this.opened = true;
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await this.openPromise;
|
|
71
|
+
} finally {
|
|
72
|
+
this.openPromise = undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async close(): Promise<void> {
|
|
77
|
+
if (this.db) {
|
|
78
|
+
const { FastSQL } = await getFastSqlModule();
|
|
79
|
+
await FastSQL.disconnect(this.name);
|
|
80
|
+
this.db = undefined;
|
|
81
|
+
this.opened = false;
|
|
82
|
+
this.openPromise = undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async execute(statement: string): Promise<void> {
|
|
87
|
+
await this.open();
|
|
88
|
+
await this.db!.execute(statement);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async run(statement: string, values: any[] = []): Promise<SQLiteRunResult> {
|
|
92
|
+
await this.open();
|
|
93
|
+
const result = await this.db!.run(statement, values as SQLValue[]);
|
|
94
|
+
return {
|
|
95
|
+
changes: result.rowsAffected,
|
|
96
|
+
lastId: result.insertId,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async query(statement: string, values: any[] = []): Promise<SQLiteQueryResult> {
|
|
101
|
+
await this.open();
|
|
102
|
+
const rows = await this.db!.query(statement, values as SQLValue[]);
|
|
103
|
+
|
|
104
|
+
// Convert array of objects to columns + values format expected by adapter
|
|
105
|
+
if (!rows.length) {
|
|
106
|
+
return { columns: [], values: [] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const columns = Object.keys(rows[0]!);
|
|
110
|
+
const resultValues = rows.map((row) => columns.map((col) => (row as Record<string, any>)[col]));
|
|
111
|
+
|
|
112
|
+
return { columns, values: resultValues };
|
|
113
|
+
}
|
|
114
|
+
}
|