@arcote.tech/arc-adapter-db-sqlite 0.3.1 → 0.3.3
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/dist/index.js +4108 -0
- package/package.json +4 -1
- package/src/bun-sqlite.ts +0 -82
- package/src/index.ts +0 -3
- package/src/sqlite-adapter.ts +0 -812
- package/tsconfig.json +0 -11
package/src/sqlite-adapter.ts
DELETED
|
@@ -1,812 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ArcContextAny,
|
|
3
|
-
type DatabaseAdapter,
|
|
4
|
-
type DatabaseAgnosticColumnInfo,
|
|
5
|
-
type DatabaseStoreData,
|
|
6
|
-
type DatabaseStoreSchema,
|
|
7
|
-
type DBAdapterFactory,
|
|
8
|
-
type FindOptions,
|
|
9
|
-
type ReadTransaction,
|
|
10
|
-
type ReadWriteTransaction,
|
|
11
|
-
type StoreColumn,
|
|
12
|
-
type StoreTable,
|
|
13
|
-
type WhereCondition,
|
|
14
|
-
extractDatabaseAgnosticSchema,
|
|
15
|
-
} from "@arcote.tech/arc";
|
|
16
|
-
|
|
17
|
-
export interface SQLiteDatabase {
|
|
18
|
-
exec(sql: string, params?: any[]): Promise<any>;
|
|
19
|
-
execBatch(queries: Array<{ sql: string; params?: any[] }>): Promise<any>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
class SQLiteReadTransaction implements ReadTransaction {
|
|
23
|
-
constructor(
|
|
24
|
-
protected db: SQLiteDatabase,
|
|
25
|
-
protected tables: Map<string, StoreTable>,
|
|
26
|
-
protected adapter?: SQLiteAdapter,
|
|
27
|
-
) {}
|
|
28
|
-
|
|
29
|
-
protected hasSoftDelete(tableName: string): boolean {
|
|
30
|
-
if (this.adapter) {
|
|
31
|
-
return this.adapter.hasSoftDelete(tableName);
|
|
32
|
-
}
|
|
33
|
-
// Fallback: check if table has deleted column
|
|
34
|
-
const table = this.tables.get(tableName);
|
|
35
|
-
if (!table) return false;
|
|
36
|
-
return table.columns.some((col) => col.name === "deleted");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
protected deserializeValue(value: any, column: StoreColumn): any {
|
|
40
|
-
if (value === null || value === undefined) return null;
|
|
41
|
-
|
|
42
|
-
switch (column.type.toLowerCase()) {
|
|
43
|
-
case "json":
|
|
44
|
-
// SQLite JSON columns should always be parsed
|
|
45
|
-
if (typeof value === "string") {
|
|
46
|
-
try {
|
|
47
|
-
return JSON.parse(value);
|
|
48
|
-
} catch {
|
|
49
|
-
return value;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return value;
|
|
53
|
-
case "text":
|
|
54
|
-
// For text columns, only attempt JSON parsing if it looks like JSON
|
|
55
|
-
if (
|
|
56
|
-
typeof value === "string" &&
|
|
57
|
-
(value.startsWith("{") || value.startsWith("["))
|
|
58
|
-
) {
|
|
59
|
-
try {
|
|
60
|
-
const parsed = JSON.parse(value);
|
|
61
|
-
if (typeof parsed === "object" || Array.isArray(parsed)) {
|
|
62
|
-
return parsed;
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
// Not valid JSON, return as string
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return value;
|
|
69
|
-
case "datetime":
|
|
70
|
-
case "timestamp":
|
|
71
|
-
return new Date(value);
|
|
72
|
-
default:
|
|
73
|
-
return value;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
protected deserializeRow(row: any, table: StoreTable): any {
|
|
78
|
-
const result: any = {};
|
|
79
|
-
for (const column of table.columns) {
|
|
80
|
-
const value = row[column.name];
|
|
81
|
-
result[column.name] = this.deserializeValue(value, column);
|
|
82
|
-
}
|
|
83
|
-
return result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
protected getId(store: string, id: any) {
|
|
87
|
-
return id;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
protected buildWhereClause(
|
|
91
|
-
where?: WhereCondition,
|
|
92
|
-
tableName?: string,
|
|
93
|
-
): {
|
|
94
|
-
sql: string;
|
|
95
|
-
params: any[];
|
|
96
|
-
} {
|
|
97
|
-
const conditions: string[] = [];
|
|
98
|
-
const params: any[] = [];
|
|
99
|
-
|
|
100
|
-
// Only add deleted condition if table has soft delete enabled
|
|
101
|
-
if (tableName && this.hasSoftDelete(tableName)) {
|
|
102
|
-
conditions.push('"deleted" = 0');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!where) {
|
|
106
|
-
return {
|
|
107
|
-
sql: conditions.length > 0 ? conditions.join(" AND ") : "1=1",
|
|
108
|
-
params,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
Object.entries(where).forEach(([key, value]) => {
|
|
113
|
-
if (typeof value === "object" && value !== null) {
|
|
114
|
-
Object.entries(value as Record<string, unknown>).forEach(
|
|
115
|
-
([operator, operand]) => {
|
|
116
|
-
switch (operator) {
|
|
117
|
-
case "$eq":
|
|
118
|
-
case "$ne":
|
|
119
|
-
case "$gt":
|
|
120
|
-
case "$gte":
|
|
121
|
-
case "$lt":
|
|
122
|
-
case "$lte":
|
|
123
|
-
conditions.push(
|
|
124
|
-
`"${key}" ${this.getOperatorSymbol(operator)} ?`,
|
|
125
|
-
);
|
|
126
|
-
params.push(operand);
|
|
127
|
-
break;
|
|
128
|
-
case "$in":
|
|
129
|
-
case "$nin":
|
|
130
|
-
if (Array.isArray(operand)) {
|
|
131
|
-
conditions.push(
|
|
132
|
-
`"${key}" ${operator === "$in" ? "IN" : "NOT IN"} (${operand.map(() => "?").join(", ")})`,
|
|
133
|
-
);
|
|
134
|
-
params.push(...operand);
|
|
135
|
-
}
|
|
136
|
-
break;
|
|
137
|
-
case "$exists":
|
|
138
|
-
if (typeof operand === "boolean") {
|
|
139
|
-
conditions.push(
|
|
140
|
-
operand ? `"${key}" IS NOT NULL` : `"${key}" IS NULL`,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
);
|
|
147
|
-
} else {
|
|
148
|
-
conditions.push(`"${key}" = ?`);
|
|
149
|
-
params.push(value);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
sql: conditions.join(" AND "),
|
|
155
|
-
params,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
protected getOperatorSymbol(operator: string): string {
|
|
160
|
-
const operators: Record<string, string> = {
|
|
161
|
-
$eq: "=",
|
|
162
|
-
$ne: "!=",
|
|
163
|
-
$gt: ">",
|
|
164
|
-
$gte: ">=",
|
|
165
|
-
$lt: "<",
|
|
166
|
-
$lte: "<=",
|
|
167
|
-
};
|
|
168
|
-
return operators[operator] || "=";
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
protected buildOrderByClause(
|
|
172
|
-
orderBy?: Record<string, "asc" | "desc">,
|
|
173
|
-
): string {
|
|
174
|
-
if (!orderBy) return "";
|
|
175
|
-
|
|
176
|
-
const orderClauses = Object.entries(orderBy)
|
|
177
|
-
.map(([key, direction]) => `"${key}" ${direction.toUpperCase()}`)
|
|
178
|
-
.join(", ");
|
|
179
|
-
|
|
180
|
-
return orderClauses ? `ORDER BY ${orderClauses}` : "";
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async find<T>(store: string, options: FindOptions<T>): Promise<T[]> {
|
|
184
|
-
const { where, limit, offset, orderBy } = options || {};
|
|
185
|
-
const whereClause = this.buildWhereClause(where, store);
|
|
186
|
-
const orderByClause = this.buildOrderByClause(orderBy);
|
|
187
|
-
const table = this.tables.get(store);
|
|
188
|
-
|
|
189
|
-
if (!table) {
|
|
190
|
-
throw new Error(`Store ${store} not found`);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const query = `
|
|
194
|
-
SELECT *
|
|
195
|
-
FROM "${table.name}"
|
|
196
|
-
WHERE ${whereClause.sql}
|
|
197
|
-
${orderByClause}
|
|
198
|
-
${limit ? `LIMIT ${limit}` : ""}
|
|
199
|
-
${offset ? `OFFSET ${offset}` : ""}
|
|
200
|
-
`;
|
|
201
|
-
|
|
202
|
-
const rows = await this.db.exec(query, whereClause.params);
|
|
203
|
-
return rows.map((row: any) => this.deserializeRow(row, table));
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
class SQLiteReadWriteTransaction
|
|
208
|
-
extends SQLiteReadTransaction
|
|
209
|
-
implements ReadWriteTransaction
|
|
210
|
-
{
|
|
211
|
-
private queries: Array<{ sql: string; params?: any[] }> = [];
|
|
212
|
-
|
|
213
|
-
constructor(
|
|
214
|
-
db: SQLiteDatabase,
|
|
215
|
-
tables: Map<string, StoreTable>,
|
|
216
|
-
protected adapter: SQLiteAdapter,
|
|
217
|
-
) {
|
|
218
|
-
super(db, tables);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async remove(store: string, id: any) {
|
|
222
|
-
const table = this.tables.get(store);
|
|
223
|
-
if (!table) {
|
|
224
|
-
throw new Error(`Store ${store} not found`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Check if table has soft delete enabled
|
|
228
|
-
const hasSoftDelete = this.adapter.hasSoftDelete(store);
|
|
229
|
-
|
|
230
|
-
if (hasSoftDelete) {
|
|
231
|
-
// Soft delete: set deleted flag
|
|
232
|
-
const query = `UPDATE "${table.name}" SET "deleted" = 1, "lastUpdate" = ? WHERE "${table.primaryKey}" = ?`;
|
|
233
|
-
this.queries.push({
|
|
234
|
-
sql: query,
|
|
235
|
-
params: [new Date().toISOString(), id],
|
|
236
|
-
});
|
|
237
|
-
} else {
|
|
238
|
-
// Hard delete: actually remove the row
|
|
239
|
-
const query = `DELETE FROM "${table.name}" WHERE "${table.primaryKey}" = ?`;
|
|
240
|
-
this.queries.push({
|
|
241
|
-
sql: query,
|
|
242
|
-
params: [id],
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async set(store: string, item: any) {
|
|
248
|
-
const table = this.tables.get(store);
|
|
249
|
-
if (!table) {
|
|
250
|
-
throw new Error(`Store ${store} not found`);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const hasVersioning = this.adapter.hasVersioning(store);
|
|
254
|
-
|
|
255
|
-
if (hasVersioning) {
|
|
256
|
-
// For versioned tables, use a transaction with version increment
|
|
257
|
-
await this.setWithVersioning(store, item, table);
|
|
258
|
-
} else {
|
|
259
|
-
// For non-versioned tables, use simple insert
|
|
260
|
-
await this.setWithoutVersioning(store, item, table);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private async setWithoutVersioning(
|
|
265
|
-
store: string,
|
|
266
|
-
item: any,
|
|
267
|
-
table: StoreTable,
|
|
268
|
-
) {
|
|
269
|
-
const columnNames = table.columns.map((col) => col.name);
|
|
270
|
-
const values = table.columns.map((column) => {
|
|
271
|
-
let value = item[column.name];
|
|
272
|
-
if (value === undefined && column.default !== undefined) {
|
|
273
|
-
value = column.default;
|
|
274
|
-
}
|
|
275
|
-
return this.serializeValue(value, column);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
const placeholders = columnNames.map(() => "?").join(", ");
|
|
279
|
-
|
|
280
|
-
// For events table, use simple INSERT since events are typically append-only
|
|
281
|
-
if (store === "events") {
|
|
282
|
-
const simpleInsertSql = `
|
|
283
|
-
INSERT INTO "${table.name}"
|
|
284
|
-
(${columnNames.map((c) => `"${c}"`).join(", ")})
|
|
285
|
-
VALUES (${placeholders})
|
|
286
|
-
`;
|
|
287
|
-
this.queries.push({
|
|
288
|
-
sql: simpleInsertSql,
|
|
289
|
-
params: values,
|
|
290
|
-
});
|
|
291
|
-
} else {
|
|
292
|
-
const sql = `
|
|
293
|
-
INSERT OR REPLACE INTO "${table.name}"
|
|
294
|
-
(${columnNames.map((c) => `"${c}"`).join(", ")})
|
|
295
|
-
VALUES (${placeholders})
|
|
296
|
-
`;
|
|
297
|
-
this.queries.push({
|
|
298
|
-
sql: sql,
|
|
299
|
-
params: values,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
private async setWithVersioning(store: string, item: any, table: StoreTable) {
|
|
305
|
-
// Filter out __version from regular columns and handle it separately
|
|
306
|
-
const regularColumns = table.columns.filter(
|
|
307
|
-
(col) => col.name !== "__version",
|
|
308
|
-
);
|
|
309
|
-
const columnNames = regularColumns.map((col) => col.name);
|
|
310
|
-
const values = regularColumns.map((column) => {
|
|
311
|
-
let value = item[column.name];
|
|
312
|
-
if (value === undefined && column.default !== undefined) {
|
|
313
|
-
value = column.default;
|
|
314
|
-
}
|
|
315
|
-
return this.serializeValue(value, column);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Add __version column
|
|
319
|
-
columnNames.push("__version");
|
|
320
|
-
const placeholders = regularColumns.map(() => "?").join(", ");
|
|
321
|
-
|
|
322
|
-
// Always increment version counter and use the new version
|
|
323
|
-
const sql = `
|
|
324
|
-
WITH next_version AS (
|
|
325
|
-
INSERT INTO __arc_version_counters (table_name, last_version)
|
|
326
|
-
VALUES (?, 1)
|
|
327
|
-
ON CONFLICT(table_name)
|
|
328
|
-
DO UPDATE SET last_version = last_version + 1
|
|
329
|
-
RETURNING last_version
|
|
330
|
-
)
|
|
331
|
-
INSERT OR REPLACE INTO "${table.name}"
|
|
332
|
-
(${columnNames.map((c) => `"${c}"`).join(", ")})
|
|
333
|
-
VALUES (${placeholders}, (SELECT last_version FROM next_version))
|
|
334
|
-
`;
|
|
335
|
-
|
|
336
|
-
this.queries.push({
|
|
337
|
-
sql: sql,
|
|
338
|
-
params: [...values, store],
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async commit() {
|
|
343
|
-
if (this.queries.length === 0) {
|
|
344
|
-
return Promise.resolve();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
try {
|
|
348
|
-
await this.db.execBatch(this.queries);
|
|
349
|
-
this.queries = [];
|
|
350
|
-
} catch (error) {
|
|
351
|
-
this.queries = [];
|
|
352
|
-
throw error;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private serializeValue(value: any, column: StoreColumn): any {
|
|
357
|
-
if (value === null || value === undefined) return null;
|
|
358
|
-
|
|
359
|
-
switch (column.type.toLowerCase()) {
|
|
360
|
-
case "timestamp":
|
|
361
|
-
case "datetime":
|
|
362
|
-
// Handle various timestamp formats
|
|
363
|
-
if (value instanceof Date) {
|
|
364
|
-
return value.toISOString();
|
|
365
|
-
}
|
|
366
|
-
if (typeof value === "number") {
|
|
367
|
-
// Unix timestamp (seconds or milliseconds)
|
|
368
|
-
const date = value > 1e10 ? new Date(value) : new Date(value * 1000);
|
|
369
|
-
return date.toISOString();
|
|
370
|
-
}
|
|
371
|
-
if (typeof value === "string") {
|
|
372
|
-
// Already an ISO string or date string, validate and normalize
|
|
373
|
-
const date = new Date(value);
|
|
374
|
-
if (!isNaN(date.getTime())) {
|
|
375
|
-
return date.toISOString();
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return value; // Pass through if we can't convert
|
|
379
|
-
case "json":
|
|
380
|
-
return JSON.stringify(value);
|
|
381
|
-
default:
|
|
382
|
-
if (value instanceof Date) {
|
|
383
|
-
return value.toISOString();
|
|
384
|
-
}
|
|
385
|
-
if (Array.isArray(value) || typeof value === "object") {
|
|
386
|
-
return JSON.stringify(value);
|
|
387
|
-
}
|
|
388
|
-
return value;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export class SQLiteAdapter implements DatabaseAdapter {
|
|
394
|
-
private tables: Map<string, StoreTable> = new Map();
|
|
395
|
-
private tableSchemas: Map<string, any[]> = new Map();
|
|
396
|
-
private pendingReinitTables: Array<{
|
|
397
|
-
tableName: string;
|
|
398
|
-
reinitFn: (tableName: string, dataStorage: any) => Promise<void>;
|
|
399
|
-
}> = [];
|
|
400
|
-
|
|
401
|
-
private mapType(arcType: string, storeData?: DatabaseStoreData): string {
|
|
402
|
-
// Check for database-specific type override
|
|
403
|
-
if (storeData?.databaseType?.sqlite) {
|
|
404
|
-
return storeData.databaseType.sqlite;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
switch (arcType) {
|
|
408
|
-
case "string":
|
|
409
|
-
case "id":
|
|
410
|
-
case "customId":
|
|
411
|
-
case "stringEnum":
|
|
412
|
-
return "TEXT";
|
|
413
|
-
case "number":
|
|
414
|
-
return "INTEGER";
|
|
415
|
-
case "boolean":
|
|
416
|
-
return "INTEGER"; // SQLite stores booleans as integers
|
|
417
|
-
case "date":
|
|
418
|
-
return "TIMESTAMP";
|
|
419
|
-
case "object":
|
|
420
|
-
case "array":
|
|
421
|
-
case "record":
|
|
422
|
-
return "JSON";
|
|
423
|
-
case "blob":
|
|
424
|
-
return "BLOB";
|
|
425
|
-
default:
|
|
426
|
-
return "TEXT";
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
private buildConstraints(storeData?: DatabaseStoreData): string[] {
|
|
431
|
-
const constraints: string[] = [];
|
|
432
|
-
|
|
433
|
-
if (storeData?.isPrimaryKey) {
|
|
434
|
-
constraints.push("PRIMARY KEY");
|
|
435
|
-
}
|
|
436
|
-
if (storeData?.isAutoIncrement) {
|
|
437
|
-
constraints.push("AUTOINCREMENT");
|
|
438
|
-
}
|
|
439
|
-
if (storeData?.isUnique) {
|
|
440
|
-
constraints.push("UNIQUE");
|
|
441
|
-
}
|
|
442
|
-
if (storeData?.foreignKey) {
|
|
443
|
-
const { table, column, onDelete, onUpdate } = storeData.foreignKey;
|
|
444
|
-
let fkConstraint = `REFERENCES ${table}(${column})`;
|
|
445
|
-
if (onDelete) fkConstraint += ` ON DELETE ${onDelete}`;
|
|
446
|
-
if (onUpdate) fkConstraint += ` ON UPDATE ${onUpdate}`;
|
|
447
|
-
constraints.push(fkConstraint);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return constraints;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
private generateColumnSQL(
|
|
454
|
-
columnInfo: DatabaseAgnosticColumnInfo & { name: string },
|
|
455
|
-
): string {
|
|
456
|
-
const type = this.mapType(columnInfo.type, columnInfo.storeData);
|
|
457
|
-
const constraints: string[] = [];
|
|
458
|
-
|
|
459
|
-
if (!columnInfo.storeData?.isNullable) {
|
|
460
|
-
constraints.push("NOT NULL");
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
constraints.push(...this.buildConstraints(columnInfo.storeData));
|
|
464
|
-
|
|
465
|
-
if (columnInfo.defaultValue !== undefined) {
|
|
466
|
-
if (typeof columnInfo.defaultValue === "string") {
|
|
467
|
-
constraints.push(`DEFAULT '${columnInfo.defaultValue}'`);
|
|
468
|
-
} else {
|
|
469
|
-
constraints.push(`DEFAULT ${columnInfo.defaultValue}`);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
return `"${columnInfo.name}" ${type} ${constraints.join(" ")}`.trim();
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
private generateCreateTableSQL(
|
|
477
|
-
tableName: string,
|
|
478
|
-
columns: (DatabaseAgnosticColumnInfo & { name: string })[],
|
|
479
|
-
): string {
|
|
480
|
-
const columnDefinitions = columns.map((col) => this.generateColumnSQL(col));
|
|
481
|
-
const indexes = columns
|
|
482
|
-
.filter((col) => col.storeData?.hasIndex && !col.storeData?.isPrimaryKey)
|
|
483
|
-
.map(
|
|
484
|
-
(col) =>
|
|
485
|
-
`CREATE INDEX IF NOT EXISTS idx_${tableName}_${col.name} ON "${tableName}"("${col.name}");`,
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
let sql = `CREATE TABLE IF NOT EXISTS "${tableName}" (\n ${columnDefinitions.join(",\n ")}\n)`;
|
|
489
|
-
|
|
490
|
-
if (indexes.length > 0) {
|
|
491
|
-
sql += ";\n" + indexes.join("\n");
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return sql;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
constructor(
|
|
498
|
-
private db: SQLiteDatabase,
|
|
499
|
-
private context: ArcContextAny,
|
|
500
|
-
) {
|
|
501
|
-
this.context.elements.forEach((element) => {
|
|
502
|
-
if (
|
|
503
|
-
"databaseStoreSchema" in element &&
|
|
504
|
-
typeof element.databaseStoreSchema === "function"
|
|
505
|
-
) {
|
|
506
|
-
const databaseSchema =
|
|
507
|
-
element.databaseStoreSchema() as DatabaseStoreSchema;
|
|
508
|
-
|
|
509
|
-
databaseSchema.tables.forEach((dbTable) => {
|
|
510
|
-
// Extract database-agnostic schema from ArcObject
|
|
511
|
-
const agnosticSchema = extractDatabaseAgnosticSchema(
|
|
512
|
-
dbTable.schema,
|
|
513
|
-
dbTable.name,
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
// Convert to database-specific columns
|
|
517
|
-
const columns = agnosticSchema.columns.map((columnInfo) => ({
|
|
518
|
-
name: columnInfo.name,
|
|
519
|
-
type: this.mapType(columnInfo.type, columnInfo.storeData),
|
|
520
|
-
constraints: this.buildConstraints(columnInfo.storeData),
|
|
521
|
-
isNullable: columnInfo.storeData?.isNullable || false,
|
|
522
|
-
defaultValue: columnInfo.defaultValue,
|
|
523
|
-
isPrimaryKey: columnInfo.storeData?.isPrimaryKey || false,
|
|
524
|
-
isAutoIncrement: columnInfo.storeData?.isAutoIncrement || false,
|
|
525
|
-
isUnique: columnInfo.storeData?.isUnique || false,
|
|
526
|
-
hasIndex: columnInfo.storeData?.hasIndex || false,
|
|
527
|
-
foreignKey: columnInfo.storeData?.foreignKey,
|
|
528
|
-
}));
|
|
529
|
-
|
|
530
|
-
this.tableSchemas.set(dbTable.name, columns);
|
|
531
|
-
|
|
532
|
-
// Convert to legacy StoreTable format for compatibility with existing transaction code
|
|
533
|
-
const legacyTable: StoreTable = {
|
|
534
|
-
name: dbTable.name,
|
|
535
|
-
primaryKey: columns.find((col) => col.isPrimaryKey)?.name || "_id",
|
|
536
|
-
columns: columns.map((col) => ({
|
|
537
|
-
name: col.name,
|
|
538
|
-
type: col.type,
|
|
539
|
-
isOptional: col.isNullable,
|
|
540
|
-
default: col.defaultValue,
|
|
541
|
-
})),
|
|
542
|
-
};
|
|
543
|
-
this.tables.set(dbTable.name, legacyTable);
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
public async initialize() {
|
|
550
|
-
// Create the version counter table first
|
|
551
|
-
await this.createVersionCounterTable();
|
|
552
|
-
// Create the table versions tracking table
|
|
553
|
-
await this.createTableVersionsTable();
|
|
554
|
-
|
|
555
|
-
const processedSchemas = new Set<DatabaseStoreSchema>();
|
|
556
|
-
const processedTables = new Set<string>();
|
|
557
|
-
|
|
558
|
-
for (const element of this.context.elements) {
|
|
559
|
-
if (
|
|
560
|
-
"databaseStoreSchema" in element &&
|
|
561
|
-
typeof element.databaseStoreSchema === "function"
|
|
562
|
-
) {
|
|
563
|
-
const databaseSchema =
|
|
564
|
-
element.databaseStoreSchema() as DatabaseStoreSchema;
|
|
565
|
-
|
|
566
|
-
// Skip if we've already processed this exact schema reference
|
|
567
|
-
if (processedSchemas.has(databaseSchema)) {
|
|
568
|
-
continue;
|
|
569
|
-
}
|
|
570
|
-
processedSchemas.add(databaseSchema);
|
|
571
|
-
|
|
572
|
-
for (const dbTable of databaseSchema.tables) {
|
|
573
|
-
const tableKey = dbTable.version
|
|
574
|
-
? `${dbTable.name}_v${dbTable.version}`
|
|
575
|
-
: dbTable.name;
|
|
576
|
-
|
|
577
|
-
if (!processedTables.has(tableKey)) {
|
|
578
|
-
// Extract database-agnostic schema from ArcObject
|
|
579
|
-
const agnosticSchema = extractDatabaseAgnosticSchema(
|
|
580
|
-
dbTable.schema,
|
|
581
|
-
dbTable.name,
|
|
582
|
-
);
|
|
583
|
-
|
|
584
|
-
// Add system columns if enabled
|
|
585
|
-
let allColumns = [...agnosticSchema.columns];
|
|
586
|
-
|
|
587
|
-
if (dbTable.options?.versioning) {
|
|
588
|
-
allColumns.push({
|
|
589
|
-
name: "__version",
|
|
590
|
-
type: "number",
|
|
591
|
-
storeData: { isNullable: false, hasIndex: true },
|
|
592
|
-
defaultValue: 1,
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (dbTable.options?.softDelete) {
|
|
597
|
-
allColumns.push({
|
|
598
|
-
name: "deleted",
|
|
599
|
-
type: "boolean",
|
|
600
|
-
storeData: { isNullable: false, hasIndex: true },
|
|
601
|
-
defaultValue: false,
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Determine physical table name based on version
|
|
606
|
-
const physicalTableName = this.getPhysicalTableName(
|
|
607
|
-
dbTable.name,
|
|
608
|
-
dbTable.version,
|
|
609
|
-
);
|
|
610
|
-
|
|
611
|
-
// Check if versioned table already exists
|
|
612
|
-
if (
|
|
613
|
-
dbTable.version &&
|
|
614
|
-
(await this.checkVersionedTableExists(
|
|
615
|
-
dbTable.name,
|
|
616
|
-
dbTable.version,
|
|
617
|
-
))
|
|
618
|
-
) {
|
|
619
|
-
console.log(
|
|
620
|
-
`Versioned table ${physicalTableName} already exists, skipping creation`,
|
|
621
|
-
);
|
|
622
|
-
} else {
|
|
623
|
-
await this.createTableIfNotExistsNew(
|
|
624
|
-
physicalTableName,
|
|
625
|
-
allColumns,
|
|
626
|
-
);
|
|
627
|
-
|
|
628
|
-
// Register the new version if it's a versioned table
|
|
629
|
-
if (dbTable.version) {
|
|
630
|
-
await this.registerTableVersion(
|
|
631
|
-
dbTable.name,
|
|
632
|
-
dbTable.version,
|
|
633
|
-
physicalTableName,
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Store reinit function for later execution (only for versioned tables)
|
|
639
|
-
// reinitTable rebuilds views from events when schema version changes
|
|
640
|
-
if (dbTable.version && databaseSchema.reinitTable) {
|
|
641
|
-
this.pendingReinitTables.push({
|
|
642
|
-
tableName: physicalTableName,
|
|
643
|
-
reinitFn: databaseSchema.reinitTable,
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Update the legacy table mapping with the actual columns (including system columns)
|
|
648
|
-
const legacyTable: StoreTable = {
|
|
649
|
-
name: physicalTableName, // Use physical table name for actual queries
|
|
650
|
-
primaryKey:
|
|
651
|
-
allColumns.find((col) => col.storeData?.isPrimaryKey)?.name ||
|
|
652
|
-
"_id",
|
|
653
|
-
columns: allColumns.map((col) => ({
|
|
654
|
-
name: col.name,
|
|
655
|
-
type: this.mapType(col.type, col.storeData),
|
|
656
|
-
isOptional: col.storeData?.isNullable || false,
|
|
657
|
-
default: col.defaultValue,
|
|
658
|
-
})),
|
|
659
|
-
};
|
|
660
|
-
this.tables.set(dbTable.name, legacyTable); // Still use logical name as key
|
|
661
|
-
|
|
662
|
-
processedTables.add(tableKey);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
private async createTableIfNotExistsNew(
|
|
670
|
-
tableName: string,
|
|
671
|
-
columns: (DatabaseAgnosticColumnInfo & { name: string })[],
|
|
672
|
-
) {
|
|
673
|
-
const createTableSQL = this.generateCreateTableSQL(tableName, columns);
|
|
674
|
-
await this.db.exec(createTableSQL);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Create the version counter table for tracking per-table version sequences
|
|
679
|
-
*/
|
|
680
|
-
private async createVersionCounterTable() {
|
|
681
|
-
const sql = `
|
|
682
|
-
CREATE TABLE IF NOT EXISTS __arc_version_counters (
|
|
683
|
-
table_name TEXT PRIMARY KEY,
|
|
684
|
-
last_version INTEGER NOT NULL DEFAULT 0
|
|
685
|
-
)
|
|
686
|
-
`;
|
|
687
|
-
await this.db.exec(sql);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Create the table versions tracking table for schema versioning
|
|
692
|
-
*/
|
|
693
|
-
private async createTableVersionsTable() {
|
|
694
|
-
const sql = `
|
|
695
|
-
CREATE TABLE IF NOT EXISTS __arc_table_versions (
|
|
696
|
-
table_name TEXT NOT NULL,
|
|
697
|
-
version INTEGER NOT NULL,
|
|
698
|
-
physical_table_name TEXT NOT NULL,
|
|
699
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
700
|
-
is_active INTEGER DEFAULT 0,
|
|
701
|
-
PRIMARY KEY (table_name, version)
|
|
702
|
-
)
|
|
703
|
-
`;
|
|
704
|
-
await this.db.exec(sql);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
/**
|
|
708
|
-
* Generate physical table name with version suffix
|
|
709
|
-
*/
|
|
710
|
-
private getPhysicalTableName(logicalName: string, version?: number): string {
|
|
711
|
-
return version ? `${logicalName}_v${version}` : logicalName;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* Check if a versioned table exists
|
|
716
|
-
*/
|
|
717
|
-
private async checkVersionedTableExists(
|
|
718
|
-
logicalName: string,
|
|
719
|
-
version: number,
|
|
720
|
-
): Promise<boolean> {
|
|
721
|
-
const result = await this.db.exec(
|
|
722
|
-
"SELECT COUNT(*) as count FROM __arc_table_versions WHERE table_name = ? AND version = ?",
|
|
723
|
-
[logicalName, version],
|
|
724
|
-
);
|
|
725
|
-
return result[0]?.count > 0;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
/**
|
|
729
|
-
* Register a new table version
|
|
730
|
-
*/
|
|
731
|
-
private async registerTableVersion(
|
|
732
|
-
logicalName: string,
|
|
733
|
-
version: number,
|
|
734
|
-
physicalName: string,
|
|
735
|
-
): Promise<void> {
|
|
736
|
-
await this.db.exec(
|
|
737
|
-
"INSERT INTO __arc_table_versions (table_name, version, physical_table_name, is_active) VALUES (?, ?, ?, ?)",
|
|
738
|
-
[logicalName, version, physicalName, 1],
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Check if a table has versioning enabled
|
|
744
|
-
*/
|
|
745
|
-
public hasVersioning(tableName: string): boolean {
|
|
746
|
-
// Check if the table has a __version column
|
|
747
|
-
const table = this.tables.get(tableName);
|
|
748
|
-
if (!table) return false;
|
|
749
|
-
return table.columns.some((col) => col.name === "__version");
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* Check if a table has soft delete enabled
|
|
754
|
-
*/
|
|
755
|
-
public hasSoftDelete(tableName: string): boolean {
|
|
756
|
-
// Check if the table has a deleted column
|
|
757
|
-
const table = this.tables.get(tableName);
|
|
758
|
-
if (!table) return false;
|
|
759
|
-
return table.columns.some((col) => col.name === "deleted");
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* Execute all pending reinitTable functions
|
|
764
|
-
*/
|
|
765
|
-
public async executeReinitTables(dataStorage: any): Promise<void> {
|
|
766
|
-
for (const { tableName, reinitFn } of this.pendingReinitTables) {
|
|
767
|
-
await reinitFn(tableName, dataStorage);
|
|
768
|
-
}
|
|
769
|
-
this.pendingReinitTables = [];
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
readWriteTransaction(stores?: string[]) {
|
|
773
|
-
return new SQLiteReadWriteTransaction(this.db, this.tables, this);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
readTransaction(stores?: string[]) {
|
|
777
|
-
return new SQLiteReadTransaction(this.db, this.tables, this);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Destroy the database - drop all tables
|
|
782
|
-
* Used for logout/reset scenarios
|
|
783
|
-
*/
|
|
784
|
-
async destroy(): Promise<void> {
|
|
785
|
-
// Drop all application tables
|
|
786
|
-
for (const tableName of this.tables.keys()) {
|
|
787
|
-
const table = this.tables.get(tableName);
|
|
788
|
-
if (table) {
|
|
789
|
-
await this.db.exec(`DROP TABLE IF EXISTS "${table.name}"`);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Drop system tables
|
|
794
|
-
await this.db.exec("DROP TABLE IF EXISTS __arc_version_counters");
|
|
795
|
-
await this.db.exec("DROP TABLE IF EXISTS __arc_table_versions");
|
|
796
|
-
|
|
797
|
-
// Clear internal state
|
|
798
|
-
this.tables.clear();
|
|
799
|
-
this.tableSchemas.clear();
|
|
800
|
-
this.pendingReinitTables = [];
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
export const createSQLiteAdapterFactory = (
|
|
805
|
-
db: SQLiteDatabase,
|
|
806
|
-
): DBAdapterFactory => {
|
|
807
|
-
return async (context: ArcContextAny): Promise<DatabaseAdapter> => {
|
|
808
|
-
const adapter = new SQLiteAdapter(db, context);
|
|
809
|
-
await adapter.initialize();
|
|
810
|
-
return adapter;
|
|
811
|
-
};
|
|
812
|
-
};
|