@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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/dist/capacitor.cjs +228 -0
  4. package/dist/capacitor.cjs.map +1 -0
  5. package/dist/capacitor.d.cts +62 -0
  6. package/dist/capacitor.d.ts +62 -0
  7. package/dist/capacitor.js +9 -0
  8. package/dist/capacitor.js.map +1 -0
  9. package/dist/chunk-LGHOZECP.js +3884 -0
  10. package/dist/chunk-LGHOZECP.js.map +1 -0
  11. package/dist/chunk-SQB6E7V2.js +191 -0
  12. package/dist/chunk-SQB6E7V2.js.map +1 -0
  13. package/dist/dexie-Bv-fV10P.d.cts +444 -0
  14. package/dist/dexie-DJFApKsM.d.ts +444 -0
  15. package/dist/dexie.cjs +381 -0
  16. package/dist/dexie.cjs.map +1 -0
  17. package/dist/dexie.d.cts +3 -0
  18. package/dist/dexie.d.ts +3 -0
  19. package/dist/dexie.js +343 -0
  20. package/dist/dexie.js.map +1 -0
  21. package/dist/expoSqlite.cjs +98 -0
  22. package/dist/expoSqlite.cjs.map +1 -0
  23. package/dist/expoSqlite.d.cts +17 -0
  24. package/dist/expoSqlite.d.ts +17 -0
  25. package/dist/expoSqlite.js +61 -0
  26. package/dist/expoSqlite.js.map +1 -0
  27. package/dist/index.cjs +3916 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +8 -0
  30. package/dist/index.d.ts +8 -0
  31. package/dist/index.js +20 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/index.shared-CPIge2ZM.d.ts +234 -0
  34. package/dist/index.shared-YSn6c01d.d.cts +234 -0
  35. package/dist/node.cjs +126 -0
  36. package/dist/node.cjs.map +1 -0
  37. package/dist/node.d.cts +80 -0
  38. package/dist/node.d.ts +80 -0
  39. package/dist/node.js +89 -0
  40. package/dist/node.js.map +1 -0
  41. package/dist/react/index.cjs +1754 -0
  42. package/dist/react/index.cjs.map +1 -0
  43. package/dist/react/index.d.cts +40 -0
  44. package/dist/react/index.d.ts +40 -0
  45. package/dist/react/index.js +78 -0
  46. package/dist/react/index.js.map +1 -0
  47. package/dist/types-CSbIAfu2.d.cts +46 -0
  48. package/dist/types-CSbIAfu2.d.ts +46 -0
  49. package/dist/wa-sqlite.cjs +318 -0
  50. package/dist/wa-sqlite.cjs.map +1 -0
  51. package/dist/wa-sqlite.d.cts +175 -0
  52. package/dist/wa-sqlite.d.ts +175 -0
  53. package/dist/wa-sqlite.js +281 -0
  54. package/dist/wa-sqlite.js.map +1 -0
  55. package/package.json +171 -0
  56. package/src/addVisibilityChangeListener.native.ts +33 -0
  57. package/src/addVisibilityChangeListener.ts +24 -0
  58. package/src/capacitor.ts +4 -0
  59. package/src/core/StateManager.ts +272 -0
  60. package/src/core/firstLoad.ts +332 -0
  61. package/src/core/pullOperations.ts +212 -0
  62. package/src/core/pushOperations.ts +290 -0
  63. package/src/core/tableEnhancers.ts +457 -0
  64. package/src/core/types.ts +3 -0
  65. package/src/createLocalId.native.ts +8 -0
  66. package/src/createLocalId.ts +6 -0
  67. package/src/dexie.ts +2 -0
  68. package/src/expoSqlite.ts +2 -0
  69. package/src/helpers.ts +87 -0
  70. package/src/index.native.ts +28 -0
  71. package/src/index.shared.ts +613 -0
  72. package/src/index.ts +28 -0
  73. package/src/logger.ts +26 -0
  74. package/src/node.ts +4 -0
  75. package/src/react/index.ts +2 -0
  76. package/src/react/useDync.ts +156 -0
  77. package/src/storage/dexie/DexieAdapter.ts +72 -0
  78. package/src/storage/dexie/DexieQueryContext.ts +14 -0
  79. package/src/storage/dexie/DexieStorageCollection.ts +124 -0
  80. package/src/storage/dexie/DexieStorageTable.ts +123 -0
  81. package/src/storage/dexie/DexieStorageWhereClause.ts +103 -0
  82. package/src/storage/dexie/helpers.ts +1 -0
  83. package/src/storage/dexie/index.ts +7 -0
  84. package/src/storage/memory/MemoryAdapter.ts +55 -0
  85. package/src/storage/memory/MemoryCollection.ts +215 -0
  86. package/src/storage/memory/MemoryQueryContext.ts +14 -0
  87. package/src/storage/memory/MemoryTable.ts +336 -0
  88. package/src/storage/memory/MemoryWhereClause.ts +134 -0
  89. package/src/storage/memory/index.ts +7 -0
  90. package/src/storage/memory/types.ts +24 -0
  91. package/src/storage/sqlite/SQLiteAdapter.ts +564 -0
  92. package/src/storage/sqlite/SQLiteCollection.ts +294 -0
  93. package/src/storage/sqlite/SQLiteTable.ts +604 -0
  94. package/src/storage/sqlite/SQLiteWhereClause.ts +341 -0
  95. package/src/storage/sqlite/SqliteQueryContext.ts +30 -0
  96. package/src/storage/sqlite/drivers/BetterSqlite3Driver.ts +156 -0
  97. package/src/storage/sqlite/drivers/CapacitorFastSqlDriver.ts +114 -0
  98. package/src/storage/sqlite/drivers/CapacitorSQLiteDriver.ts +137 -0
  99. package/src/storage/sqlite/drivers/ExpoSQLiteDriver.native.ts +67 -0
  100. package/src/storage/sqlite/drivers/WaSqliteDriver.ts +537 -0
  101. package/src/storage/sqlite/drivers/wa-sqlite-vfs.d.ts +46 -0
  102. package/src/storage/sqlite/helpers.ts +144 -0
  103. package/src/storage/sqlite/index.ts +11 -0
  104. package/src/storage/sqlite/schema.ts +44 -0
  105. package/src/storage/sqlite/types.ts +164 -0
  106. package/src/storage/types.ts +112 -0
  107. package/src/types.ts +186 -0
  108. package/src/wa-sqlite.ts +4 -0
@@ -0,0 +1,604 @@
1
+ import type { StorageTable, StorageWhereClause, StorageCollection } from '../types';
2
+ import { LOCAL_PK } from '../../types';
3
+ import type { SQLiteTableSchemaMetadata, SQLiteIterateEntriesOptions, SQLiteOrderByOptions, SQLiteCollectionState, TableEntry } from './types';
4
+ import { SQLiteAdapter } from './SQLiteAdapter';
5
+ import { SQLiteCollection } from './SQLiteCollection';
6
+ import { SQLiteWhereClause } from './SQLiteWhereClause';
7
+ import { cloneValue, createDefaultState, normalizeComparableValue, quoteIdentifier, DEFAULT_STREAM_BATCH_SIZE } from './helpers';
8
+
9
+ export class SQLiteTable<T = any> implements StorageTable<T> {
10
+ readonly name: string;
11
+ readonly schema: SQLiteTableSchemaMetadata;
12
+ readonly hook: unknown = Object.freeze({});
13
+ readonly raw: {
14
+ add: (item: T) => Promise<unknown>;
15
+ put: (item: T) => Promise<unknown>;
16
+ update: (key: unknown, changes: Partial<T>) => Promise<number>;
17
+ delete: (key: unknown) => Promise<void>;
18
+ get: (key: unknown) => Promise<T | undefined>;
19
+ bulkAdd: (items: T[]) => Promise<unknown>;
20
+ bulkPut: (items: T[]) => Promise<unknown>;
21
+ bulkUpdate: (keysAndChanges: Array<{ key: unknown; changes: Partial<T> }>) => Promise<number>;
22
+ bulkDelete: (keys: Array<unknown>) => Promise<void>;
23
+ clear: () => Promise<void>;
24
+ };
25
+
26
+ private readonly adapter: SQLiteAdapter;
27
+ private readonly columnNames: string[];
28
+ private readonly booleanColumns: Set<string>;
29
+
30
+ constructor(adapter: SQLiteAdapter, schema: SQLiteTableSchemaMetadata) {
31
+ this.adapter = adapter;
32
+ this.schema = schema;
33
+ this.name = schema.name;
34
+ this.columnNames = Object.keys(schema.definition.columns ?? {});
35
+ // Track which columns are declared as BOOLEAN for read/write conversion
36
+ this.booleanColumns = new Set(
37
+ Object.entries(schema.definition.columns ?? {})
38
+ .filter(([_, col]) => col.type?.toUpperCase() === 'BOOLEAN')
39
+ .map(([name]) => name),
40
+ );
41
+ // Capture bound methods BEFORE any wrapping can occur
42
+ // These provide access to the underlying storage operations
43
+ this.raw = Object.freeze({
44
+ add: this.baseAdd.bind(this),
45
+ put: this.basePut.bind(this),
46
+ update: this.baseUpdate.bind(this),
47
+ delete: this.baseDelete.bind(this),
48
+ get: this.get.bind(this),
49
+ bulkAdd: this.baseBulkAdd.bind(this),
50
+ bulkPut: this.baseBulkPut.bind(this),
51
+ bulkUpdate: this.baseBulkUpdate.bind(this),
52
+ bulkDelete: this.baseBulkDelete.bind(this),
53
+ clear: this.baseClear.bind(this),
54
+ });
55
+ }
56
+
57
+ async add(item: T): Promise<unknown> {
58
+ return this.baseAdd(item);
59
+ }
60
+
61
+ async put(item: T): Promise<unknown> {
62
+ return this.basePut(item);
63
+ }
64
+
65
+ async update(key: unknown, changes: Partial<T>): Promise<number> {
66
+ return this.baseUpdate(key, changes);
67
+ }
68
+
69
+ async delete(key: unknown): Promise<void> {
70
+ await this.baseDelete(key);
71
+ }
72
+
73
+ async clear(): Promise<void> {
74
+ await this.baseClear();
75
+ }
76
+
77
+ private async baseClear(): Promise<void> {
78
+ await this.adapter.execute(`DELETE FROM ${quoteIdentifier(this.name)}`);
79
+ }
80
+
81
+ async get(key: unknown): Promise<T | undefined> {
82
+ if (!key || typeof key !== 'string') {
83
+ return undefined;
84
+ }
85
+ const row = await this.fetchRow(key);
86
+ return row ? this.cloneRecord(row) : undefined;
87
+ }
88
+
89
+ async toArray(): Promise<T[]> {
90
+ const entries = await this.getEntries();
91
+ return entries.map((entry) => this.cloneRecord(entry.value));
92
+ }
93
+
94
+ async count(): Promise<number> {
95
+ const rows = await this.adapter.queryRows(`SELECT COUNT(*) as count FROM ${quoteIdentifier(this.name)}`);
96
+ return Number(rows[0]?.count ?? 0);
97
+ }
98
+
99
+ async bulkAdd(items: T[]): Promise<unknown> {
100
+ return this.baseBulkAdd(items);
101
+ }
102
+
103
+ private async baseBulkAdd(items: T[]): Promise<unknown> {
104
+ if (!items.length) return undefined;
105
+
106
+ const columns = this.columnNames;
107
+ const columnCount = columns.length;
108
+ // SQLite has a parameter limit (typically 999 or 32766). Use conservative batch size.
109
+ const maxParamsPerBatch = 500;
110
+ const batchSize = Math.max(1, Math.floor(maxParamsPerBatch / columnCount));
111
+
112
+ let lastKey: unknown = undefined;
113
+
114
+ for (let i = 0; i < items.length; i += batchSize) {
115
+ const batch = items.slice(i, i + batchSize);
116
+ const records = batch.map((item) => this.prepareRecordForWrite(item));
117
+
118
+ const placeholderRow = `(${columns.map(() => '?').join(', ')})`;
119
+ const placeholders = records.map(() => placeholderRow).join(', ');
120
+ const values: unknown[] = [];
121
+
122
+ for (const record of records) {
123
+ values.push(...this.extractColumnValues(record));
124
+ }
125
+
126
+ await this.adapter.run(
127
+ `INSERT INTO ${quoteIdentifier(this.name)} (${columns.map((c) => quoteIdentifier(c)).join(', ')}) VALUES ${placeholders}`,
128
+ values,
129
+ );
130
+
131
+ lastKey = (records[records.length - 1] as any)[LOCAL_PK];
132
+ }
133
+
134
+ return lastKey;
135
+ }
136
+
137
+ async bulkPut(items: T[]): Promise<unknown> {
138
+ return this.baseBulkPut(items);
139
+ }
140
+
141
+ private async baseBulkPut(items: T[]): Promise<unknown> {
142
+ if (!items.length) return undefined;
143
+
144
+ const columns = this.columnNames;
145
+ const columnCount = columns.length;
146
+ const maxParamsPerBatch = 500;
147
+ const batchSize = Math.max(1, Math.floor(maxParamsPerBatch / columnCount));
148
+
149
+ let lastKey: unknown = undefined;
150
+
151
+ for (let i = 0; i < items.length; i += batchSize) {
152
+ const batch = items.slice(i, i + batchSize);
153
+ const records = batch.map((item) => this.prepareRecordForWrite(item));
154
+
155
+ const placeholderRow = `(${columns.map(() => '?').join(', ')})`;
156
+ const placeholders = records.map(() => placeholderRow).join(', ');
157
+ const values: unknown[] = [];
158
+
159
+ for (const record of records) {
160
+ values.push(...this.extractColumnValues(record));
161
+ }
162
+
163
+ await this.adapter.run(
164
+ `INSERT OR REPLACE INTO ${quoteIdentifier(this.name)} (${columns.map((c) => quoteIdentifier(c)).join(', ')}) VALUES ${placeholders}`,
165
+ values,
166
+ );
167
+
168
+ lastKey = (records[records.length - 1] as any)[LOCAL_PK];
169
+ }
170
+
171
+ return lastKey;
172
+ }
173
+
174
+ async bulkGet(keys: Array<unknown>): Promise<Array<T | undefined>> {
175
+ if (!keys.length) return [];
176
+
177
+ // Use IN clause for bulk lookup
178
+ const validKeys = keys.filter((k) => k && typeof k === 'string');
179
+ if (!validKeys.length) return keys.map(() => undefined);
180
+
181
+ const selectClause = this.buildSelectClause();
182
+ const placeholders = validKeys.map(() => '?').join(', ');
183
+
184
+ const rows = await this.adapter.queryRows(
185
+ `SELECT ${selectClause} FROM ${quoteIdentifier(this.name)} WHERE ${quoteIdentifier(LOCAL_PK)} IN (${placeholders})`,
186
+ validKeys,
187
+ );
188
+
189
+ // Build a map of key -> record for quick lookup
190
+ const recordMap = new Map<string, T>();
191
+ for (const row of rows) {
192
+ const record = this.hydrateRow(row);
193
+ recordMap.set(String(row[LOCAL_PK]), this.cloneRecord(record));
194
+ }
195
+
196
+ // Return results in the same order as input keys
197
+ return keys.map((key) => (key && typeof key === 'string' ? recordMap.get(key) : undefined));
198
+ }
199
+
200
+ async bulkUpdate(keysAndChanges: Array<{ key: unknown; changes: Partial<T> }>): Promise<number> {
201
+ return this.baseBulkUpdate(keysAndChanges);
202
+ }
203
+
204
+ private async baseBulkUpdate(keysAndChanges: Array<{ key: unknown; changes: Partial<T> }>): Promise<number> {
205
+ if (!keysAndChanges.length) return 0;
206
+
207
+ let updatedCount = 0;
208
+ for (const { key, changes } of keysAndChanges) {
209
+ const result = await this.baseUpdate(key, changes);
210
+ updatedCount += result;
211
+ }
212
+ return updatedCount;
213
+ }
214
+
215
+ async bulkDelete(keys: Array<unknown>): Promise<void> {
216
+ await this.baseBulkDelete(keys);
217
+ }
218
+
219
+ private async baseBulkDelete(keys: Array<unknown>): Promise<void> {
220
+ if (!keys.length) return;
221
+
222
+ const validKeys = keys.filter((k) => k && typeof k === 'string');
223
+ if (!validKeys.length) return;
224
+
225
+ const placeholders = validKeys.map(() => '?').join(', ');
226
+ await this.adapter.run(`DELETE FROM ${quoteIdentifier(this.name)} WHERE ${quoteIdentifier(LOCAL_PK)} IN (${placeholders})`, validKeys);
227
+ }
228
+
229
+ where(index: string | string[]): StorageWhereClause<T> {
230
+ return this.createWhereClause(index);
231
+ }
232
+
233
+ orderBy(index: string | string[]): StorageCollection<T> {
234
+ return this.createCollection({
235
+ orderBy: { index, direction: 'asc' },
236
+ });
237
+ }
238
+
239
+ reverse(): StorageCollection<T> {
240
+ return this.createCollection({ reverse: true });
241
+ }
242
+
243
+ offset(offset: number): StorageCollection<T> {
244
+ return this.createCollection({ offset });
245
+ }
246
+
247
+ limit(count: number): StorageCollection<T> {
248
+ return this.createCollection({ limit: count });
249
+ }
250
+
251
+ mapToClass(_ctor: new (...args: any[]) => any): StorageTable<T> {
252
+ return this;
253
+ }
254
+
255
+ async each(callback: (item: T) => void | Promise<void>): Promise<void> {
256
+ const entries = await this.getEntries();
257
+ for (const entry of entries) {
258
+ await callback(this.cloneRecord(entry.value));
259
+ }
260
+ }
261
+
262
+ jsFilter(predicate: (item: T) => boolean): StorageCollection<T> {
263
+ return this.createCollection({ jsPredicate: (record) => predicate(record) });
264
+ }
265
+
266
+ createCollection(stateOverrides?: Partial<SQLiteCollectionState<T>>): SQLiteCollection<T> {
267
+ return new SQLiteCollection(this, stateOverrides);
268
+ }
269
+
270
+ createCollectionFromPredicate(predicate: (record: T, key: string, index: number) => boolean, template?: SQLiteCollection<T>): SQLiteCollection<T> {
271
+ const baseState = template ? template.getState() : createDefaultState<T>();
272
+ const existingPredicate = baseState.jsPredicate;
273
+ const combinedPredicate = existingPredicate
274
+ ? (record: T, key: string, index: number) => existingPredicate(record, key, index) && predicate(record, key, index)
275
+ : predicate;
276
+ return new SQLiteCollection(this, {
277
+ ...baseState,
278
+ jsPredicate: combinedPredicate,
279
+ });
280
+ }
281
+
282
+ createWhereClause(index: string | string[], baseCollection?: SQLiteCollection<T>): SQLiteWhereClause<T> {
283
+ return new SQLiteWhereClause(this, index, baseCollection);
284
+ }
285
+
286
+ async *iterateEntries(options?: SQLiteIterateEntriesOptions): AsyncGenerator<TableEntry<T>> {
287
+ const selectClause = this.buildSelectClause();
288
+ const chunkSize = options?.chunkSize ?? DEFAULT_STREAM_BATCH_SIZE;
289
+ const orderClause = this.buildOrderByClause(options?.orderBy);
290
+ let offset = 0;
291
+
292
+ while (true) {
293
+ const statementParts = [`SELECT ${selectClause} FROM ${quoteIdentifier(this.name)}`];
294
+ if (orderClause) {
295
+ statementParts.push(orderClause);
296
+ }
297
+ statementParts.push(`LIMIT ${chunkSize} OFFSET ${offset}`);
298
+ const rows = await this.adapter.queryRows(statementParts.join(' '));
299
+ if (!rows.length) {
300
+ break;
301
+ }
302
+ for (const row of rows) {
303
+ yield { key: String(row[LOCAL_PK]), value: this.hydrateRow(row) };
304
+ }
305
+ if (rows.length < chunkSize) {
306
+ break;
307
+ }
308
+ offset += rows.length;
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Execute a query with pre-built WHERE clause and parameters.
314
+ * This is used by SQLiteCollection for native SQL performance.
315
+ */
316
+ async queryWithConditions(options: {
317
+ whereClause: string;
318
+ parameters: unknown[];
319
+ orderBy?: { index: string | string[]; direction: 'asc' | 'desc' };
320
+ limit?: number;
321
+ offset?: number;
322
+ distinct?: boolean;
323
+ }): Promise<T[]> {
324
+ const selectClause = this.buildSelectClause();
325
+ const distinctKeyword = options.distinct ? 'DISTINCT ' : '';
326
+ const parts = [`SELECT ${distinctKeyword}${selectClause} FROM ${quoteIdentifier(this.name)}`];
327
+
328
+ if (options.whereClause) {
329
+ parts.push(options.whereClause);
330
+ }
331
+
332
+ if (options.orderBy) {
333
+ parts.push(this.buildOrderByClause(options.orderBy));
334
+ }
335
+
336
+ if (options.limit !== undefined) {
337
+ parts.push(`LIMIT ${options.limit}`);
338
+ }
339
+
340
+ if (options.offset !== undefined && options.offset > 0) {
341
+ parts.push(`OFFSET ${options.offset}`);
342
+ }
343
+
344
+ const rows = await this.adapter.queryRows(parts.join(' '), options.parameters);
345
+ return rows.map((row) => this.hydrateRow(row));
346
+ }
347
+
348
+ /**
349
+ * Execute a COUNT query with pre-built WHERE clause.
350
+ */
351
+ async countWithConditions(options: { whereClause: string; parameters: unknown[]; distinct?: boolean }): Promise<number> {
352
+ const distinctKeyword = options.distinct ? 'DISTINCT ' : '';
353
+ const selectClause = this.buildSelectClause();
354
+ // For DISTINCT, we need to count distinct rows, not just COUNT(*)
355
+ const countExpr = options.distinct ? `COUNT(${distinctKeyword}${selectClause})` : 'COUNT(*)';
356
+ const parts = [`SELECT ${countExpr} as count FROM ${quoteIdentifier(this.name)}`];
357
+
358
+ if (options.whereClause) {
359
+ parts.push(options.whereClause);
360
+ }
361
+
362
+ const rows = await this.adapter.queryRows(parts.join(' '), options.parameters);
363
+ return Number(rows[0]?.count ?? 0);
364
+ }
365
+
366
+ /**
367
+ * Execute a DELETE query with pre-built WHERE clause.
368
+ * Returns the number of deleted rows.
369
+ */
370
+ async deleteWithConditions(options: { whereClause: string; parameters: unknown[] }): Promise<number> {
371
+ const parts = [`DELETE FROM ${quoteIdentifier(this.name)}`];
372
+
373
+ if (options.whereClause) {
374
+ parts.push(options.whereClause);
375
+ }
376
+
377
+ const result = await this.adapter.run(parts.join(' '), options.parameters);
378
+ return result.changes ?? 0;
379
+ }
380
+
381
+ /**
382
+ * Execute an UPDATE query with pre-built WHERE clause.
383
+ * Returns the number of updated rows.
384
+ */
385
+ async updateWithConditions(options: { whereClause: string; parameters: unknown[]; changes: Partial<T> }): Promise<number> {
386
+ const changeEntries = Object.entries(options.changes);
387
+ if (changeEntries.length === 0) {
388
+ return 0;
389
+ }
390
+
391
+ const setClauses = changeEntries.map(([column]) => `${quoteIdentifier(column)} = ?`);
392
+ const setValues = changeEntries.map(([, value]) => this.normalizeColumnValue(value));
393
+
394
+ const parts = [`UPDATE ${quoteIdentifier(this.name)} SET ${setClauses.join(', ')}`];
395
+
396
+ if (options.whereClause) {
397
+ parts.push(options.whereClause);
398
+ }
399
+
400
+ const result = await this.adapter.run(parts.join(' '), [...setValues, ...options.parameters]);
401
+ return result.changes ?? 0;
402
+ }
403
+
404
+ /**
405
+ * Query only primary keys with pre-built WHERE clause.
406
+ * This is more efficient than fetching full rows when only keys are needed.
407
+ */
408
+ async queryKeysWithConditions(options: {
409
+ whereClause: string;
410
+ parameters: unknown[];
411
+ orderBy?: { index: string | string[]; direction: 'asc' | 'desc' };
412
+ limit?: number;
413
+ offset?: number;
414
+ distinct?: boolean;
415
+ }): Promise<string[]> {
416
+ const distinctKeyword = options.distinct ? 'DISTINCT ' : '';
417
+ const parts = [`SELECT ${distinctKeyword}${quoteIdentifier(LOCAL_PK)} FROM ${quoteIdentifier(this.name)}`];
418
+
419
+ if (options.whereClause) {
420
+ parts.push(options.whereClause);
421
+ }
422
+
423
+ if (options.orderBy) {
424
+ parts.push(this.buildOrderByClause(options.orderBy));
425
+ }
426
+
427
+ if (options.limit !== undefined) {
428
+ parts.push(`LIMIT ${options.limit}`);
429
+ }
430
+
431
+ if (options.offset !== undefined && options.offset > 0) {
432
+ parts.push(`OFFSET ${options.offset}`);
433
+ }
434
+
435
+ const rows = await this.adapter.queryRows(parts.join(' '), options.parameters);
436
+ return rows.map((row) => String(row[LOCAL_PK]));
437
+ }
438
+
439
+ async getEntries(): Promise<TableEntry<T>[]> {
440
+ const entries: TableEntry<T>[] = [];
441
+ for await (const entry of this.iterateEntries()) {
442
+ entries.push(entry);
443
+ }
444
+ return entries;
445
+ }
446
+
447
+ cloneRecord(record: T): T {
448
+ return cloneValue(record);
449
+ }
450
+
451
+ compareValues(left: unknown, right: unknown): number {
452
+ const normalizedLeft = normalizeComparableValue(left) as any;
453
+ const normalizedRight = normalizeComparableValue(right) as any;
454
+ if (normalizedLeft < normalizedRight) return -1;
455
+ if (normalizedLeft > normalizedRight) return 1;
456
+ return 0;
457
+ }
458
+
459
+ compareByIndex(left: T, right: T, index: string | string[]): number {
460
+ if (Array.isArray(index)) {
461
+ for (const key of index) {
462
+ const diff = this.compareValues((left as any)[key], (right as any)[key]);
463
+ if (diff !== 0) {
464
+ return diff;
465
+ }
466
+ }
467
+ return 0;
468
+ }
469
+ return this.compareValues((left as any)[index], (right as any)[index]);
470
+ }
471
+
472
+ getIndexValue(record: T, index: string | string[]): unknown {
473
+ if (Array.isArray(index)) {
474
+ return index.map((key) => (record as any)[key]);
475
+ }
476
+ return (record as any)[index];
477
+ }
478
+
479
+ async replaceRecord(record: T): Promise<void> {
480
+ if (!this.columnNames.length) {
481
+ return;
482
+ }
483
+ const assignments = this.columnNames.map((column) => `${quoteIdentifier(column)} = ?`).join(', ');
484
+ const values = this.extractColumnValues(record);
485
+ await this.adapter.run(`UPDATE ${quoteIdentifier(this.name)} SET ${assignments} WHERE ${quoteIdentifier(LOCAL_PK)} = ?`, [
486
+ ...values,
487
+ (record as any)[LOCAL_PK],
488
+ ]);
489
+ }
490
+
491
+ async deleteByPrimaryKey(primaryKey: string): Promise<void> {
492
+ await this.adapter.run(`DELETE FROM ${quoteIdentifier(this.name)} WHERE ${quoteIdentifier(LOCAL_PK)} = ?`, [primaryKey]);
493
+ }
494
+
495
+ private async baseAdd(item: T): Promise<string> {
496
+ const record = this.prepareRecordForWrite(item);
497
+ const columns = this.columnNames;
498
+ const placeholders = columns.map(() => '?').join(', ');
499
+ const values = this.extractColumnValues(record);
500
+ await this.adapter.run(
501
+ `INSERT INTO ${quoteIdentifier(this.name)} (${columns.map((column) => quoteIdentifier(column)).join(', ')}) VALUES (${placeholders})`,
502
+ values,
503
+ );
504
+ return (record as any)[LOCAL_PK];
505
+ }
506
+
507
+ private async basePut(item: T): Promise<string> {
508
+ const record = this.prepareRecordForWrite(item);
509
+ const columns = this.columnNames;
510
+ const placeholders = columns.map(() => '?').join(', ');
511
+ const values = this.extractColumnValues(record);
512
+ await this.adapter.run(
513
+ `INSERT OR REPLACE INTO ${quoteIdentifier(this.name)} (${columns.map((column) => quoteIdentifier(column)).join(', ')}) VALUES (${placeholders})`,
514
+ values,
515
+ );
516
+ return (record as any)[LOCAL_PK];
517
+ }
518
+
519
+ private async baseUpdate(key: unknown, changes: Partial<T>): Promise<number> {
520
+ if (!key || typeof key !== 'string') {
521
+ return 0;
522
+ }
523
+ const existing = await this.fetchRow(key);
524
+ if (!existing) {
525
+ return 0;
526
+ }
527
+ const updated = { ...existing, ...changes } as T;
528
+ await this.replaceRecord(updated);
529
+ return 1;
530
+ }
531
+
532
+ private async baseDelete(key: unknown): Promise<void> {
533
+ if (!key || typeof key !== 'string') {
534
+ return;
535
+ }
536
+ await this.deleteByPrimaryKey(key);
537
+ }
538
+
539
+ private prepareRecordForWrite(item: T): T {
540
+ const clone = this.cloneRecord(item);
541
+ const primaryValue = (clone as any)[LOCAL_PK];
542
+
543
+ if (!primaryValue || typeof primaryValue !== 'string') {
544
+ throw new Error(`Missing required primary key field "${LOCAL_PK}" - a string value must be provided`);
545
+ }
546
+
547
+ return clone;
548
+ }
549
+
550
+ private async fetchRow(primaryKey: string): Promise<T | undefined> {
551
+ const selectClause = this.buildSelectClause();
552
+ const rows = await this.adapter.queryRows(`SELECT ${selectClause} FROM ${quoteIdentifier(this.name)} WHERE ${quoteIdentifier(LOCAL_PK)} = ? LIMIT 1`, [
553
+ primaryKey,
554
+ ]);
555
+ if (!rows.length) {
556
+ return undefined;
557
+ }
558
+ return this.hydrateRow(rows[0]!);
559
+ }
560
+
561
+ private buildSelectClause(): string {
562
+ const dataColumns = this.columnNames.map((column) => quoteIdentifier(column));
563
+ return dataColumns.join(', ');
564
+ }
565
+
566
+ private hydrateRow(row: Record<string, any>): T {
567
+ const record: Record<string, any> = {};
568
+ for (const column of this.columnNames) {
569
+ let value = row[column];
570
+ // Convert INTEGER back to boolean for BOOLEAN columns
571
+ if (this.booleanColumns.has(column) && value !== null && value !== undefined) {
572
+ value = value === 1 || value === true;
573
+ }
574
+ record[column] = value;
575
+ }
576
+ return record as T;
577
+ }
578
+
579
+ private buildOrderByClause(orderBy?: SQLiteOrderByOptions): string {
580
+ const target = orderBy ?? { index: LOCAL_PK, direction: 'asc' as const };
581
+ const columns = Array.isArray(target.index) ? target.index : [target.index];
582
+ const direction = target.direction.toUpperCase();
583
+ const clause = columns.map((column) => `${quoteIdentifier(column)} ${direction}`).join(', ');
584
+ return `ORDER BY ${clause}`;
585
+ }
586
+
587
+ private extractColumnValues(record: T): any[] {
588
+ return this.columnNames.map((column) => this.normalizeColumnValue((record as any)[column]));
589
+ }
590
+
591
+ private normalizeColumnValue(value: unknown): unknown {
592
+ if (value === undefined) {
593
+ return null;
594
+ }
595
+ if (value instanceof Date) {
596
+ return value.toISOString();
597
+ }
598
+ // Convert boolean to INTEGER for SQLite storage
599
+ if (typeof value === 'boolean') {
600
+ return value ? 1 : 0;
601
+ }
602
+ return value;
603
+ }
604
+ }