@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,7 @@
|
|
|
1
|
+
export { MemoryAdapter } from './MemoryAdapter';
|
|
2
|
+
export { MemoryQueryContext } from './MemoryQueryContext';
|
|
3
|
+
export { MemoryCollection } from './MemoryCollection';
|
|
4
|
+
export { MemoryWhereClause } from './MemoryWhereClause';
|
|
5
|
+
export { MemoryTable } from './MemoryTable';
|
|
6
|
+
export { createDefaultState } from './types';
|
|
7
|
+
export type { MemoryRecord, MemoryCollectionState } from './types';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface MemoryRecord {
|
|
2
|
+
_localId?: string;
|
|
3
|
+
id?: unknown;
|
|
4
|
+
updated_at?: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface MemoryCollectionState<T extends MemoryRecord> {
|
|
9
|
+
predicate: (record: T, key: string) => boolean;
|
|
10
|
+
orderBy?: { index: string | string[]; direction: 'asc' | 'desc' };
|
|
11
|
+
reverse: boolean;
|
|
12
|
+
offset: number;
|
|
13
|
+
limit?: number;
|
|
14
|
+
distinct: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const createDefaultState = <T extends MemoryRecord>(): MemoryCollectionState<T> => ({
|
|
18
|
+
predicate: () => true,
|
|
19
|
+
orderBy: undefined,
|
|
20
|
+
reverse: false,
|
|
21
|
+
offset: 0,
|
|
22
|
+
limit: undefined,
|
|
23
|
+
distinct: false,
|
|
24
|
+
});
|
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import type { StorageAdapter, StorageTable, StorageTransactionContext, TransactionMode } from '../types';
|
|
2
|
+
import type {
|
|
3
|
+
TableSchemaDefinition,
|
|
4
|
+
SQLiteTableDefinition,
|
|
5
|
+
SQLiteColumnDefinition,
|
|
6
|
+
SQLiteIndexDefinition,
|
|
7
|
+
SQLiteDefaultValue,
|
|
8
|
+
SQLiteForeignKeyReference,
|
|
9
|
+
} from './schema';
|
|
10
|
+
import type { StorageSchemaDefinitionOptions, SQLiteMigrationContext, SQLiteMigrationDirection, SQLiteMigrationHandler } from './types';
|
|
11
|
+
import type { SQLiteDatabaseDriver, SQLiteRunResult, SQLiteQueryResult } from './types';
|
|
12
|
+
import { LOCAL_PK } from '../../types';
|
|
13
|
+
import type { SQLiteAdapterOptions, SQLiteTableSchemaMetadata, SQLiteColumnSchema } from './types';
|
|
14
|
+
import { DYNC_STATE_TABLE } from '../../core/StateManager';
|
|
15
|
+
import { SQLITE_SCHEMA_VERSION_STATE_KEY, quoteIdentifier } from './helpers';
|
|
16
|
+
import { SQLiteTable } from './SQLiteTable';
|
|
17
|
+
import { SqliteQueryContext } from './SqliteQueryContext';
|
|
18
|
+
|
|
19
|
+
export class SQLiteAdapter implements StorageAdapter {
|
|
20
|
+
readonly type = 'SQLiteAdapter';
|
|
21
|
+
readonly name: string;
|
|
22
|
+
|
|
23
|
+
private readonly options: SQLiteAdapterOptions;
|
|
24
|
+
private readonly schemas = new Map<string, SQLiteTableSchemaMetadata>();
|
|
25
|
+
private readonly versionSchemas = new Map<number, Map<string, SQLiteTableSchemaMetadata>>();
|
|
26
|
+
private readonly versionOptions = new Map<number, StorageSchemaDefinitionOptions>();
|
|
27
|
+
private readonly tableCache = new Map<string, SQLiteTable<any>>();
|
|
28
|
+
private driver: SQLiteDatabaseDriver;
|
|
29
|
+
private openPromise?: Promise<void>;
|
|
30
|
+
private isOpen = false;
|
|
31
|
+
private schemaApplied = false;
|
|
32
|
+
private transactionDepth = 0;
|
|
33
|
+
private targetVersion = 0;
|
|
34
|
+
|
|
35
|
+
constructor(driver: SQLiteDatabaseDriver, options: SQLiteAdapterOptions = {}) {
|
|
36
|
+
this.driver = driver;
|
|
37
|
+
this.name = driver.name;
|
|
38
|
+
this.options = options;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get driverType(): string {
|
|
42
|
+
return this.driver.type;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Opens the database connection and applies schema.
|
|
47
|
+
* This is called automatically when performing operations,
|
|
48
|
+
* so explicit calls are optional but safe (idempotent).
|
|
49
|
+
* When called explicitly after schema changes, it will run any pending migrations.
|
|
50
|
+
*/
|
|
51
|
+
async open(): Promise<void> {
|
|
52
|
+
if (this.isOpen) {
|
|
53
|
+
// Explicit open() call while already open - check for pending migrations
|
|
54
|
+
await this.runPendingMigrations();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
return this.ensureOpen();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private async ensureOpen(): Promise<void> {
|
|
61
|
+
if (this.isOpen) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (this.openPromise) {
|
|
65
|
+
return this.openPromise;
|
|
66
|
+
}
|
|
67
|
+
this.openPromise = this.performOpen();
|
|
68
|
+
try {
|
|
69
|
+
await this.openPromise;
|
|
70
|
+
} finally {
|
|
71
|
+
this.openPromise = undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async performOpen(): Promise<void> {
|
|
76
|
+
await this.driver.open();
|
|
77
|
+
await this.applySchema();
|
|
78
|
+
await this.runPendingMigrations();
|
|
79
|
+
this.isOpen = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async close(): Promise<void> {
|
|
83
|
+
if (this.driver) {
|
|
84
|
+
await this.driver.close();
|
|
85
|
+
}
|
|
86
|
+
this.isOpen = false;
|
|
87
|
+
this.tableCache.clear();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async delete(): Promise<void> {
|
|
91
|
+
for (const table of this.schemas.keys()) {
|
|
92
|
+
await this.execute(`DROP TABLE IF EXISTS ${quoteIdentifier(table)}`);
|
|
93
|
+
}
|
|
94
|
+
await this.execute(`DROP TABLE IF EXISTS ${quoteIdentifier(DYNC_STATE_TABLE)}`);
|
|
95
|
+
|
|
96
|
+
this.tableCache.clear();
|
|
97
|
+
this.schemaApplied = false;
|
|
98
|
+
// Rebuild schemas from versionSchemas so the adapter remains usable.
|
|
99
|
+
// The next operation will re-create the tables via applySchema().
|
|
100
|
+
this.refreshActiveSchema();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
defineSchema(version: number, schema: Record<string, TableSchemaDefinition>, options?: StorageSchemaDefinitionOptions): void {
|
|
104
|
+
const normalized = new Map<string, SQLiteTableSchemaMetadata>();
|
|
105
|
+
for (const [tableName, definition] of Object.entries(schema)) {
|
|
106
|
+
if (typeof definition === 'string') {
|
|
107
|
+
throw new Error(`SQLite adapter requires structured schema definitions. Table '${tableName}' must provide an object-based schema.`);
|
|
108
|
+
}
|
|
109
|
+
normalized.set(tableName, this.parseStructuredSchema(tableName, definition));
|
|
110
|
+
}
|
|
111
|
+
this.versionSchemas.set(version, normalized);
|
|
112
|
+
this.versionOptions.set(version, options ?? {});
|
|
113
|
+
this.refreshActiveSchema();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private refreshActiveSchema(): void {
|
|
117
|
+
if (!this.versionSchemas.size) {
|
|
118
|
+
this.schemas.clear();
|
|
119
|
+
this.targetVersion = 0;
|
|
120
|
+
this.schemaApplied = false;
|
|
121
|
+
this.tableCache.clear();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const versions = Array.from(this.versionSchemas.keys());
|
|
126
|
+
const latestVersion = Math.max(...versions);
|
|
127
|
+
const latestSchema = this.versionSchemas.get(latestVersion);
|
|
128
|
+
if (!latestSchema) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.schemas.clear();
|
|
133
|
+
for (const [name, schema] of latestSchema.entries()) {
|
|
134
|
+
this.schemas.set(name, schema);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (this.targetVersion !== latestVersion) {
|
|
138
|
+
this.tableCache.clear();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.targetVersion = latestVersion;
|
|
142
|
+
this.schemaApplied = false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
table<T = any>(name: string): StorageTable<T> {
|
|
146
|
+
if (!this.schemas.has(name)) {
|
|
147
|
+
throw new Error(`Table '${name}' is not part of the defined schema`);
|
|
148
|
+
}
|
|
149
|
+
if (!this.tableCache.has(name)) {
|
|
150
|
+
this.tableCache.set(name, new SQLiteTable<T>(this, this.schemas.get(name)!));
|
|
151
|
+
}
|
|
152
|
+
return this.tableCache.get(name)! as StorageTable<T>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async transaction<T>(_mode: TransactionMode, tableNames: string[], callback: (context: StorageTransactionContext) => Promise<T>): Promise<T> {
|
|
156
|
+
const driver = await this.getDriver();
|
|
157
|
+
const shouldManageTransaction = this.transactionDepth === 0;
|
|
158
|
+
this.transactionDepth += 1;
|
|
159
|
+
if (shouldManageTransaction) {
|
|
160
|
+
this.logSql('BEGIN TRANSACTION');
|
|
161
|
+
await driver.execute('BEGIN TRANSACTION');
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const tables: Record<string, StorageTable<any>> = {};
|
|
165
|
+
for (const tableName of tableNames) {
|
|
166
|
+
tables[tableName] = this.table(tableName);
|
|
167
|
+
}
|
|
168
|
+
const result = await callback({ tables });
|
|
169
|
+
if (shouldManageTransaction) {
|
|
170
|
+
this.logSql('COMMIT');
|
|
171
|
+
await driver.execute('COMMIT');
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (shouldManageTransaction) {
|
|
176
|
+
this.logSql('ROLLBACK');
|
|
177
|
+
await driver.execute('ROLLBACK');
|
|
178
|
+
}
|
|
179
|
+
throw err;
|
|
180
|
+
} finally {
|
|
181
|
+
this.transactionDepth -= 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async execute(statement: string, values?: any[]): Promise<void> {
|
|
186
|
+
if (values && values.length) {
|
|
187
|
+
await this.run(statement, values);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const driver = await this.getDriver();
|
|
191
|
+
this.logSql(statement);
|
|
192
|
+
await driver.execute(statement);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async run(statement: string, values?: any[]): Promise<SQLiteRunResult> {
|
|
196
|
+
const driver = await this.getDriver();
|
|
197
|
+
const params = values ?? [];
|
|
198
|
+
this.logSql(statement, params);
|
|
199
|
+
return driver.run(statement, params);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async query<R>(callback: (ctx: SqliteQueryContext) => Promise<R>): Promise<R>;
|
|
203
|
+
async query(statement: string, values?: any[]): Promise<SQLiteQueryResult>;
|
|
204
|
+
async query<R>(arg1: string | ((ctx: SqliteQueryContext) => Promise<R>), arg2?: any[]): Promise<R | SQLiteQueryResult> {
|
|
205
|
+
if (typeof arg1 === 'function') {
|
|
206
|
+
const driver = await this.getDriver();
|
|
207
|
+
return arg1(new SqliteQueryContext(driver, this));
|
|
208
|
+
}
|
|
209
|
+
const statement = arg1;
|
|
210
|
+
const values = arg2;
|
|
211
|
+
const driver = await this.getDriver();
|
|
212
|
+
const params = values ?? [];
|
|
213
|
+
this.logSql(statement, params);
|
|
214
|
+
return driver.query(statement, params);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async queryRows(statement: string, values?: any[]): Promise<Array<Record<string, any>>> {
|
|
218
|
+
const result = await this.query(statement, values);
|
|
219
|
+
const columns = result.columns ?? [];
|
|
220
|
+
return (result.values ?? []).map((row) => {
|
|
221
|
+
const record: Record<string, any> = {};
|
|
222
|
+
for (let index = 0; index < columns.length; index += 1) {
|
|
223
|
+
record[columns[index]!] = row[index];
|
|
224
|
+
}
|
|
225
|
+
return record;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Ensures the database is open and returns the driver.
|
|
231
|
+
* This is the main entry point for all public database operations.
|
|
232
|
+
*/
|
|
233
|
+
private async getDriver(): Promise<SQLiteDatabaseDriver> {
|
|
234
|
+
await this.ensureOpen();
|
|
235
|
+
return this.driver;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Internal execute that uses driver directly.
|
|
240
|
+
* Used during the open process to avoid recursion.
|
|
241
|
+
*/
|
|
242
|
+
private async internalExecute(statement: string, values?: any[]): Promise<void> {
|
|
243
|
+
if (values && values.length) {
|
|
244
|
+
await this.internalRun(statement, values);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.logSql(statement);
|
|
248
|
+
await this.driver.execute(statement);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Internal run that uses driver directly.
|
|
253
|
+
* Used during the open process to avoid recursion.
|
|
254
|
+
*/
|
|
255
|
+
private async internalRun(statement: string, values?: any[]): Promise<SQLiteRunResult> {
|
|
256
|
+
const params = values ?? [];
|
|
257
|
+
this.logSql(statement, params);
|
|
258
|
+
return this.driver.run(statement, params);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Internal queryRows that uses driver directly.
|
|
263
|
+
* Used during the open process to avoid recursion.
|
|
264
|
+
*/
|
|
265
|
+
private async internalQueryRows(statement: string, values?: any[]): Promise<Array<Record<string, any>>> {
|
|
266
|
+
const params = values ?? [];
|
|
267
|
+
this.logSql(statement, params);
|
|
268
|
+
const result = await this.driver.query(statement, params);
|
|
269
|
+
const columns = result.columns ?? [];
|
|
270
|
+
return (result.values ?? []).map((row) => {
|
|
271
|
+
const record: Record<string, any> = {};
|
|
272
|
+
for (let index = 0; index < columns.length; index += 1) {
|
|
273
|
+
record[columns[index]!] = row[index];
|
|
274
|
+
}
|
|
275
|
+
return record;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Internal query that uses driver directly.
|
|
281
|
+
* Used during migrations to avoid recursion.
|
|
282
|
+
*/
|
|
283
|
+
private async internalQuery(statement: string, values?: any[]): Promise<SQLiteQueryResult> {
|
|
284
|
+
const params = values ?? [];
|
|
285
|
+
this.logSql(statement, params);
|
|
286
|
+
return this.driver.query(statement, params);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private logSql(statement: string, parameters?: any[]): void {
|
|
290
|
+
const { debug } = this.options;
|
|
291
|
+
if (!debug) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const hasParams = parameters && parameters.length;
|
|
295
|
+
if (typeof debug === 'function') {
|
|
296
|
+
debug(statement, hasParams ? parameters : undefined);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (debug === true) {
|
|
300
|
+
if (hasParams) {
|
|
301
|
+
console.debug('[dync][sqlite]', statement, parameters);
|
|
302
|
+
} else {
|
|
303
|
+
console.debug('[dync][sqlite]', statement);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async getStoredSchemaVersion(): Promise<number> {
|
|
309
|
+
const rows = await this.internalQueryRows(`SELECT value FROM ${quoteIdentifier(DYNC_STATE_TABLE)} WHERE ${quoteIdentifier(LOCAL_PK)} = ? LIMIT 1`, [
|
|
310
|
+
SQLITE_SCHEMA_VERSION_STATE_KEY,
|
|
311
|
+
]);
|
|
312
|
+
const rawValue = rows[0]?.value;
|
|
313
|
+
const parsed = Number(rawValue ?? 0);
|
|
314
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async setStoredSchemaVersion(version: number): Promise<void> {
|
|
318
|
+
await this.internalRun(
|
|
319
|
+
`INSERT INTO ${quoteIdentifier(DYNC_STATE_TABLE)} (${quoteIdentifier(LOCAL_PK)}, value) VALUES (?, ?) ON CONFLICT(${quoteIdentifier(LOCAL_PK)}) DO UPDATE SET value = excluded.value`,
|
|
320
|
+
[SQLITE_SCHEMA_VERSION_STATE_KEY, String(version)],
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private async runPendingMigrations(): Promise<void> {
|
|
325
|
+
if (!this.versionSchemas.size) {
|
|
326
|
+
await this.setStoredSchemaVersion(0);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const targetVersion = this.targetVersion;
|
|
331
|
+
const currentVersion = await this.getStoredSchemaVersion();
|
|
332
|
+
|
|
333
|
+
if (currentVersion === targetVersion) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (currentVersion < targetVersion) {
|
|
338
|
+
for (let version = currentVersion + 1; version <= targetVersion; version += 1) {
|
|
339
|
+
await this.runMigrationStep(version, 'upgrade', version - 1, version);
|
|
340
|
+
await this.setStoredSchemaVersion(version);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for (let version = currentVersion; version > targetVersion; version -= 1) {
|
|
346
|
+
await this.runMigrationStep(version, 'downgrade', version, version - 1);
|
|
347
|
+
await this.setStoredSchemaVersion(version - 1);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private async runMigrationStep(version: number, direction: SQLiteMigrationDirection, fromVersion: number, toVersion: number): Promise<void> {
|
|
352
|
+
const handler = this.getMigrationHandler(version, direction);
|
|
353
|
+
if (!handler) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const context: SQLiteMigrationContext = {
|
|
357
|
+
direction,
|
|
358
|
+
fromVersion,
|
|
359
|
+
toVersion,
|
|
360
|
+
execute: (statement: string) => this.internalExecute(statement),
|
|
361
|
+
run: (statement: string, values?: any[]) => this.internalRun(statement, values),
|
|
362
|
+
query: (statement: string, values?: any[]) => this.internalQuery(statement, values),
|
|
363
|
+
};
|
|
364
|
+
await handler(context);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private getMigrationHandler(version: number, direction: SQLiteMigrationDirection): SQLiteMigrationHandler | undefined {
|
|
368
|
+
const options = this.versionOptions.get(version);
|
|
369
|
+
const migrations = options?.sqlite?.migrations;
|
|
370
|
+
if (!migrations) {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
return direction === 'upgrade' ? migrations.upgrade : migrations.downgrade;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private async applySchema(): Promise<void> {
|
|
377
|
+
if (this.schemaApplied) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
for (const schema of this.schemas.values()) {
|
|
381
|
+
await this.internalExecute(this.buildCreateTableStatement(schema));
|
|
382
|
+
const indexStatements = this.buildIndexStatements(schema);
|
|
383
|
+
for (const statement of indexStatements) {
|
|
384
|
+
await this.internalExecute(statement);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
this.schemaApplied = true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private buildCreateTableStatement(schema: SQLiteTableSchemaMetadata): string {
|
|
391
|
+
const columns: string[] = [];
|
|
392
|
+
|
|
393
|
+
for (const column of Object.values(schema.definition.columns)) {
|
|
394
|
+
columns.push(this.buildStructuredColumnDefinition(column));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (schema.definition.source === 'structured' && Array.isArray(schema.definition.tableConstraints)) {
|
|
398
|
+
columns.push(...schema.definition.tableConstraints.filter(Boolean));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const trailingClauses = [schema.definition.withoutRowId ? 'WITHOUT ROWID' : undefined, schema.definition.strict ? 'STRICT' : undefined].filter(Boolean);
|
|
402
|
+
|
|
403
|
+
const suffix = trailingClauses.length ? ` ${trailingClauses.join(' ')}` : '';
|
|
404
|
+
return `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(schema.name)} (${columns.join(', ')})${suffix}`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private buildStructuredColumnDefinition(column: SQLiteColumnSchema): string {
|
|
408
|
+
const parts: string[] = [];
|
|
409
|
+
parts.push(quoteIdentifier(column.name));
|
|
410
|
+
parts.push(this.formatColumnType(column));
|
|
411
|
+
|
|
412
|
+
if (column.name === LOCAL_PK) {
|
|
413
|
+
parts.push('PRIMARY KEY');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (column.collate) {
|
|
417
|
+
parts.push(`COLLATE ${column.collate}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (column.generatedAlwaysAs) {
|
|
421
|
+
const storage = column.stored ? 'STORED' : 'VIRTUAL';
|
|
422
|
+
parts.push(`GENERATED ALWAYS AS (${column.generatedAlwaysAs}) ${storage}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (column.unique) {
|
|
426
|
+
parts.push('UNIQUE');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (column.default !== undefined) {
|
|
430
|
+
parts.push(`DEFAULT ${this.formatDefaultValue(column.default)}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (column.check) {
|
|
434
|
+
parts.push(`CHECK (${column.check})`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (column.references) {
|
|
438
|
+
parts.push(this.buildReferencesClause(column.references));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (column.constraints?.length) {
|
|
442
|
+
parts.push(...column.constraints);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return parts.filter(Boolean).join(' ');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private formatColumnType(column: SQLiteColumnSchema): string {
|
|
449
|
+
const declaredType = column.type?.trim().toUpperCase();
|
|
450
|
+
// Map BOOLEAN to INTEGER for SQLite storage (SQLite has no native boolean type)
|
|
451
|
+
const base = !declaredType || !declaredType.length ? 'NUMERIC' : declaredType === 'BOOLEAN' ? 'INTEGER' : declaredType;
|
|
452
|
+
if (column.length && !base.includes('(')) {
|
|
453
|
+
return `${base}(${column.length})`;
|
|
454
|
+
}
|
|
455
|
+
return base;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private formatDefaultValue(value: SQLiteDefaultValue): string {
|
|
459
|
+
if (value === null) {
|
|
460
|
+
return 'NULL';
|
|
461
|
+
}
|
|
462
|
+
if (typeof value === 'number') {
|
|
463
|
+
return String(value);
|
|
464
|
+
}
|
|
465
|
+
if (typeof value === 'boolean') {
|
|
466
|
+
return value ? '1' : '0';
|
|
467
|
+
}
|
|
468
|
+
const escaped = String(value).replace(/'/g, "''");
|
|
469
|
+
return `'${escaped}'`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private buildReferencesClause(reference: SQLiteForeignKeyReference | string): string {
|
|
473
|
+
if (typeof reference === 'string') {
|
|
474
|
+
return `REFERENCES ${reference}`;
|
|
475
|
+
}
|
|
476
|
+
const parts: string[] = [];
|
|
477
|
+
parts.push(`REFERENCES ${quoteIdentifier(reference.table)}`);
|
|
478
|
+
if (reference.column) {
|
|
479
|
+
parts.push(`(${quoteIdentifier(reference.column)})`);
|
|
480
|
+
}
|
|
481
|
+
if (reference.match) {
|
|
482
|
+
parts.push(`MATCH ${reference.match}`);
|
|
483
|
+
}
|
|
484
|
+
if (reference.onDelete) {
|
|
485
|
+
parts.push(`ON DELETE ${reference.onDelete}`);
|
|
486
|
+
}
|
|
487
|
+
if (reference.onUpdate) {
|
|
488
|
+
parts.push(`ON UPDATE ${reference.onUpdate}`);
|
|
489
|
+
}
|
|
490
|
+
return parts.join(' ');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private buildIndexStatements(schema: SQLiteTableSchemaMetadata): string[] {
|
|
494
|
+
if (schema.definition.source !== 'structured' || !schema.definition.indexes?.length) {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
const statements: string[] = [];
|
|
498
|
+
schema.definition.indexes.forEach((index, position) => {
|
|
499
|
+
if (!index.columns?.length) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const indexName = this.generateIndexName(schema, index, position);
|
|
503
|
+
const columnSegments = index.columns.map((columnName, columnIndex) => {
|
|
504
|
+
const segments = [quoteIdentifier(columnName)];
|
|
505
|
+
if (index.collate) {
|
|
506
|
+
segments.push(`COLLATE ${index.collate}`);
|
|
507
|
+
}
|
|
508
|
+
const order = index.orders?.[columnIndex];
|
|
509
|
+
if (order) {
|
|
510
|
+
segments.push(order);
|
|
511
|
+
}
|
|
512
|
+
return segments.join(' ');
|
|
513
|
+
});
|
|
514
|
+
const whereClause = index.where ? ` WHERE ${index.where}` : '';
|
|
515
|
+
statements.push(
|
|
516
|
+
`CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(schema.name)} (${columnSegments.join(', ')})${whereClause}`,
|
|
517
|
+
);
|
|
518
|
+
});
|
|
519
|
+
return statements;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private generateIndexName(schema: SQLiteTableSchemaMetadata, index: SQLiteIndexDefinition, position: number): string {
|
|
523
|
+
if (index.name) {
|
|
524
|
+
return index.name;
|
|
525
|
+
}
|
|
526
|
+
const sanitizedColumns = index.columns.map((column) => column.replace(/[^A-Za-z0-9_]/g, '_')).join('_');
|
|
527
|
+
const suffix = sanitizedColumns || String(position);
|
|
528
|
+
return `${schema.name}_${suffix}_idx`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private parseStructuredSchema(tableName: string, definition: SQLiteTableDefinition): SQLiteTableSchemaMetadata {
|
|
532
|
+
if (!definition?.columns || !Object.keys(definition.columns).length) {
|
|
533
|
+
throw new Error(`SQLite schema for table '${tableName}' must define at least one column.`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (!definition.columns[LOCAL_PK]) {
|
|
537
|
+
throw new Error(`SQLite schema for table '${tableName}' must define a column named '${LOCAL_PK}'.`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const normalizedColumns = this.normalizeColumns(definition.columns);
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
name: tableName,
|
|
544
|
+
definition: {
|
|
545
|
+
...definition,
|
|
546
|
+
name: tableName,
|
|
547
|
+
columns: normalizedColumns,
|
|
548
|
+
source: 'structured',
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private normalizeColumns(columns: Record<string, SQLiteColumnDefinition>): Record<string, SQLiteColumnSchema> {
|
|
554
|
+
const normalized: Record<string, SQLiteColumnSchema> = {};
|
|
555
|
+
for (const [name, column] of Object.entries(columns)) {
|
|
556
|
+
normalized[name] = {
|
|
557
|
+
name,
|
|
558
|
+
...column,
|
|
559
|
+
nullable: column?.nullable ?? true,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return normalized;
|
|
563
|
+
}
|
|
564
|
+
}
|