@zmdb/sqlite 1.0.0-beta.1
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 +674 -0
- package/README.md +108 -0
- package/dist/dialect.d.ts +2 -0
- package/dist/dialect.d.ts.map +1 -0
- package/dist/dialect.js +93 -0
- package/dist/dialect.js.map +1 -0
- package/dist/driver.d.ts +21 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +133 -0
- package/dist/driver.js.map +1 -0
- package/dist/embedded.d.ts +3 -0
- package/dist/embedded.d.ts.map +1 -0
- package/dist/embedded.js +2 -0
- package/dist/embedded.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/introspector.d.ts +5 -0
- package/dist/introspector.d.ts.map +1 -0
- package/dist/introspector.js +392 -0
- package/dist/introspector.js.map +1 -0
- package/dist/migrations.d.ts +3 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +349 -0
- package/dist/migrations.js.map +1 -0
- package/dist/node.d.ts +2 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +2 -0
- package/dist/node.js.map +1 -0
- package/package.json +61 -0
- package/src/dialect.ts +95 -0
- package/src/driver.ts +158 -0
- package/src/embedded.ts +2 -0
- package/src/index.ts +14 -0
- package/src/introspector.ts +513 -0
- package/src/migrations.ts +500 -0
- package/src/node.ts +1 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChangeOp,
|
|
3
|
+
ColumnSnapshot,
|
|
4
|
+
ExtensionType,
|
|
5
|
+
ForeignKeySnapshot,
|
|
6
|
+
ReferentialAction,
|
|
7
|
+
SchemaSnapshot,
|
|
8
|
+
} from '@zmdb/migrations';
|
|
9
|
+
import {
|
|
10
|
+
UnsupportedFeatureError,
|
|
11
|
+
type AppliedMigration,
|
|
12
|
+
type MigrationConnection,
|
|
13
|
+
type MigrationDialect,
|
|
14
|
+
type MigrationDriver,
|
|
15
|
+
type MigrationPlan,
|
|
16
|
+
type MigrationTableOptions,
|
|
17
|
+
type SchemaObjectOperation,
|
|
18
|
+
} from '@zmdb/sql';
|
|
19
|
+
import { type IndexColumn, type IndexDef, type RoutineDef } from '@zmdb/sql/schema-objects';
|
|
20
|
+
|
|
21
|
+
const SQLITE_TYPES = Object.freeze({
|
|
22
|
+
serial: 'INTEGER',
|
|
23
|
+
integer: 'INTEGER',
|
|
24
|
+
bigint: 'INTEGER',
|
|
25
|
+
numeric: 'NUMERIC',
|
|
26
|
+
text: 'TEXT',
|
|
27
|
+
varchar: 'TEXT',
|
|
28
|
+
boolean: 'INTEGER',
|
|
29
|
+
timestamp: 'TEXT',
|
|
30
|
+
json: 'TEXT',
|
|
31
|
+
jsonEnum: 'TEXT',
|
|
32
|
+
} as const);
|
|
33
|
+
|
|
34
|
+
const EXTENSION_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
35
|
+
|
|
36
|
+
function q(identifier: string): string {
|
|
37
|
+
return `"${identifier.replaceAll('"', '""')}"`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extensionTypeDdl(type: ExtensionType): string {
|
|
41
|
+
if (!EXTENSION_IDENTIFIER.test(type.name)) {
|
|
42
|
+
throw new TypeError(`extension type name ${JSON.stringify(type.name)} is not a SQL identifier`);
|
|
43
|
+
}
|
|
44
|
+
const rendered = (type.args ?? []).map(argument => {
|
|
45
|
+
if (typeof argument === 'number' && Number.isFinite(argument)) return String(argument);
|
|
46
|
+
if (typeof argument === 'string' && EXTENSION_IDENTIFIER.test(argument)) return argument;
|
|
47
|
+
throw new TypeError(
|
|
48
|
+
`extension type ${type.name} argument ${JSON.stringify(argument)} must be a finite number or SQL identifier`,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
return `${type.name}${rendered.length === 0 ? '' : `(${rendered.join(',')})`}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function unsupportedExtensionType(type: ExtensionType, column: string, table?: string): never {
|
|
55
|
+
const rendered = extensionTypeDdl(type);
|
|
56
|
+
const location = table === undefined ? `column "${column}"` : `"${table}"."${column}"`;
|
|
57
|
+
throw new UnsupportedFeatureError(
|
|
58
|
+
`extension type ${rendered}`,
|
|
59
|
+
'sqlite',
|
|
60
|
+
`sqlite does not support the extension type ${rendered} on ${location} (extension \`${type.extension}\`); ` +
|
|
61
|
+
'there is no equivalent, and storing it as TEXT would produce a value the database cannot use',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sqliteDdlType(column: ColumnSnapshot): string {
|
|
66
|
+
if (typeof column.type !== 'string') return unsupportedExtensionType(column.type, column.name);
|
|
67
|
+
const mapped: unknown = Reflect.get(SQLITE_TYPES, column.type);
|
|
68
|
+
return typeof mapped === 'string' ? mapped : column.type;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function refuseNonRowidSerial(column: string, table: string): never {
|
|
72
|
+
throw new UnsupportedFeatureError(
|
|
73
|
+
`serial column "${table}"."${column}" outside a sole primary key`,
|
|
74
|
+
'sqlite',
|
|
75
|
+
`sqlite can generate serial values only for a sole INTEGER PRIMARY KEY; ` +
|
|
76
|
+
`"${table}"."${column}" is not that key, and SQLite has no standalone sequence or column identity`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function columnDdl(
|
|
81
|
+
column: ColumnSnapshot,
|
|
82
|
+
table: string,
|
|
83
|
+
key: { readonly inline: boolean; readonly tableLevel: boolean },
|
|
84
|
+
): string {
|
|
85
|
+
if (typeof column.type !== 'string') unsupportedExtensionType(column.type, column.name, table);
|
|
86
|
+
if (column.type === 'serial' && !key.inline) refuseNonRowidSerial(column.name, table);
|
|
87
|
+
// Only the exact spelling INTEGER PRIMARY KEY aliases SQLite's rowid. Keep a
|
|
88
|
+
// supplied integer key as INT so it cannot silently acquire serial behavior.
|
|
89
|
+
const type = key.inline && column.type === 'integer' ? 'INT' : sqliteDdlType(column);
|
|
90
|
+
const rowidPrimaryKey = key.inline && column.type === 'serial';
|
|
91
|
+
const primaryKey = key.inline ? ' PRIMARY KEY' : '';
|
|
92
|
+
const notNull = rowidPrimaryKey || (!key.inline && column.nullable && !key.tableLevel) ? '' : ' NOT NULL';
|
|
93
|
+
return `${q(column.name)} ${type}${primaryKey}${notNull}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function primaryKeyDdl(columns: readonly string[]): string {
|
|
97
|
+
return `PRIMARY KEY (${columns.map(q).join(', ')})`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function actionName(action: ReferentialAction): string {
|
|
101
|
+
return action.toUpperCase();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function foreignKeyDdl(foreignKey: ForeignKeySnapshot): string {
|
|
105
|
+
if (foreignKey.columns.length === 0 || foreignKey.columns.length !== foreignKey.targetColumns.length) {
|
|
106
|
+
throw new TypeError(
|
|
107
|
+
`foreign key "${foreignKey.name}" must have the same non-zero number of local and target columns`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return (
|
|
111
|
+
`FOREIGN KEY (${foreignKey.columns.map(q).join(', ')}) ` +
|
|
112
|
+
`REFERENCES ${q(foreignKey.targetTable)} (${foreignKey.targetColumns.map(q).join(', ')}) ` +
|
|
113
|
+
`ON DELETE ${actionName(foreignKey.onDelete)} ON UPDATE ${actionName(foreignKey.onUpdate)}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createTableDdl(operation: Extract<ChangeOp, { readonly kind: 'create_table' }>): string {
|
|
118
|
+
if (operation.tableOptions !== undefined) {
|
|
119
|
+
throw new UnsupportedFeatureError(
|
|
120
|
+
`table options on "${operation.table}"`,
|
|
121
|
+
'sqlite',
|
|
122
|
+
`sqlite does not support shard keys, sort keys, or rowstore table options on "${operation.table}"`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const inline = operation.primaryKey.length === 1 ? operation.primaryKey[0] : undefined;
|
|
126
|
+
const tableLevel = operation.primaryKey.length > 1 ? new Set(operation.primaryKey) : undefined;
|
|
127
|
+
const definitions = operation.columns.map(column =>
|
|
128
|
+
columnDdl(column, operation.table, {
|
|
129
|
+
inline: column.name === inline,
|
|
130
|
+
tableLevel: tableLevel?.has(column.name) === true,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
if (operation.primaryKey.length > 1) definitions.push(primaryKeyDdl(operation.primaryKey));
|
|
134
|
+
definitions.push(...operation.foreignKeys.map(foreignKeyDdl));
|
|
135
|
+
return `CREATE TABLE ${q(operation.table)} (${definitions.join(', ')})`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function keyList(columns: readonly string[]): string {
|
|
139
|
+
return `(${columns.join(', ')})`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function refuseAlterPrimaryKey(table: string, from: readonly string[], to: readonly string[]): never {
|
|
143
|
+
throw new UnsupportedFeatureError(
|
|
144
|
+
`altering the primary key of "${table}"`,
|
|
145
|
+
'sqlite',
|
|
146
|
+
`sqlite cannot alter the primary key of "${table}" (${keyList(from)} → ${keyList(to)}); ` +
|
|
147
|
+
'SQLite has no ALTER TABLE form for a key, so this needs a hand-written table rebuild — ' +
|
|
148
|
+
'see the migration guide',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function refuseForeignKey(action: 'add' | 'drop', table: string, foreignKey: ForeignKeySnapshot | string): never {
|
|
153
|
+
const name = typeof foreignKey === 'string' ? foreignKey : foreignKey.name;
|
|
154
|
+
throw new UnsupportedFeatureError(
|
|
155
|
+
`${action === 'add' ? 'adding' : 'dropping'} foreign key "${name}" on "${table}"`,
|
|
156
|
+
'sqlite',
|
|
157
|
+
`sqlite cannot ${action} the foreign key "${name}" on "${table}"; ` +
|
|
158
|
+
'SQLite has no ALTER TABLE form for a constraint, so this needs a hand-written table rebuild — ' +
|
|
159
|
+
'see the migration guide',
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function refuseRecreateDroppedTable(table: string): never {
|
|
164
|
+
throw new UnsupportedFeatureError(
|
|
165
|
+
`recreating dropped table "${table}"`,
|
|
166
|
+
'sqlite',
|
|
167
|
+
`sqlite cannot recreate dropped table "${table}" because the drop operation carries no columns; ` +
|
|
168
|
+
'write the down migration by hand',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function refuseRecreateDroppedColumn(table: string, column: string): never {
|
|
173
|
+
throw new UnsupportedFeatureError(
|
|
174
|
+
`recreating dropped column "${table}"."${column}"`,
|
|
175
|
+
'sqlite',
|
|
176
|
+
`sqlite cannot recreate dropped column "${table}"."${column}" because the drop operation carries no type, ` +
|
|
177
|
+
'nullability, key, or default metadata; write the down migration by hand',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function validateSnapshot(snapshot: SchemaSnapshot): void {
|
|
182
|
+
if (snapshot.extensions.length > 0) {
|
|
183
|
+
const extension = snapshot.extensions[0];
|
|
184
|
+
throw new UnsupportedFeatureError(
|
|
185
|
+
`extension "${extension?.name ?? 'unknown'}"`,
|
|
186
|
+
'sqlite',
|
|
187
|
+
`sqlite does not support database extensions ("${extension?.name ?? 'unknown'}")`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
for (const table of snapshot.tables) {
|
|
191
|
+
if (table.tableOptions !== undefined) {
|
|
192
|
+
throw new UnsupportedFeatureError(
|
|
193
|
+
`table options on "${table.name}"`,
|
|
194
|
+
'sqlite',
|
|
195
|
+
`sqlite does not support shard keys, sort keys, or rowstore table options on "${table.name}"`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
for (const column of table.columns) {
|
|
199
|
+
if (typeof column.type !== 'string') unsupportedExtensionType(column.type, column.name, table.name);
|
|
200
|
+
if (column.type === 'serial' && (table.primaryKey.length !== 1 || table.primaryKey[0] !== column.name)) {
|
|
201
|
+
refuseNonRowidSerial(column.name, table.name);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function validatePlan(plan: MigrationPlan): void {
|
|
208
|
+
validateSnapshot(plan.before);
|
|
209
|
+
validateSnapshot(plan.after);
|
|
210
|
+
|
|
211
|
+
for (const operation of plan.operations) {
|
|
212
|
+
switch (operation.kind) {
|
|
213
|
+
case 'create_extension':
|
|
214
|
+
throw new UnsupportedFeatureError(
|
|
215
|
+
`extension "${operation.name}"`,
|
|
216
|
+
'sqlite',
|
|
217
|
+
`sqlite does not support database extensions ("${operation.name}")`,
|
|
218
|
+
);
|
|
219
|
+
case 'alter_column_type':
|
|
220
|
+
throw new UnsupportedFeatureError(
|
|
221
|
+
'alter column type',
|
|
222
|
+
'sqlite',
|
|
223
|
+
'sqlite cannot alter a column type in place; use a hand-written table rebuild',
|
|
224
|
+
);
|
|
225
|
+
case 'alter_primary_key':
|
|
226
|
+
refuseAlterPrimaryKey(operation.table, operation.from, operation.to);
|
|
227
|
+
case 'add_foreign_key':
|
|
228
|
+
refuseForeignKey('add', operation.table, operation.fk);
|
|
229
|
+
case 'drop_foreign_key':
|
|
230
|
+
refuseForeignKey('drop', operation.table, operation.name);
|
|
231
|
+
default:
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function emitUp(operation: ChangeOp): string {
|
|
238
|
+
switch (operation.kind) {
|
|
239
|
+
case 'create_extension':
|
|
240
|
+
throw new UnsupportedFeatureError(
|
|
241
|
+
`extension "${operation.name}"`,
|
|
242
|
+
'sqlite',
|
|
243
|
+
`sqlite does not support database extensions ("${operation.name}")`,
|
|
244
|
+
);
|
|
245
|
+
case 'create_table':
|
|
246
|
+
return createTableDdl(operation);
|
|
247
|
+
case 'drop_table':
|
|
248
|
+
return `DROP TABLE ${q(operation.table)}`;
|
|
249
|
+
case 'add_column':
|
|
250
|
+
return `ALTER TABLE ${q(operation.table)} ADD COLUMN ${columnDdl(operation.column, operation.table, {
|
|
251
|
+
inline: false,
|
|
252
|
+
tableLevel: false,
|
|
253
|
+
})}`;
|
|
254
|
+
case 'drop_column':
|
|
255
|
+
return `ALTER TABLE ${q(operation.table)} DROP COLUMN ${q(operation.column)}`;
|
|
256
|
+
case 'alter_column_type':
|
|
257
|
+
throw new UnsupportedFeatureError(
|
|
258
|
+
'alter column type',
|
|
259
|
+
'sqlite',
|
|
260
|
+
'sqlite cannot alter a column type in place; use a hand-written table rebuild',
|
|
261
|
+
);
|
|
262
|
+
case 'alter_primary_key':
|
|
263
|
+
return refuseAlterPrimaryKey(operation.table, operation.from, operation.to);
|
|
264
|
+
case 'add_foreign_key':
|
|
265
|
+
return refuseForeignKey('add', operation.table, operation.fk);
|
|
266
|
+
case 'drop_foreign_key':
|
|
267
|
+
return refuseForeignKey('drop', operation.table, operation.name);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function emitDown(operation: ChangeOp): string {
|
|
272
|
+
switch (operation.kind) {
|
|
273
|
+
case 'create_extension':
|
|
274
|
+
throw new UnsupportedFeatureError(
|
|
275
|
+
`extension "${operation.name}"`,
|
|
276
|
+
'sqlite',
|
|
277
|
+
`sqlite does not support database extensions ("${operation.name}")`,
|
|
278
|
+
);
|
|
279
|
+
case 'create_table':
|
|
280
|
+
return `DROP TABLE ${q(operation.table)}`;
|
|
281
|
+
case 'drop_table':
|
|
282
|
+
return refuseRecreateDroppedTable(operation.table);
|
|
283
|
+
case 'add_column':
|
|
284
|
+
return `ALTER TABLE ${q(operation.table)} DROP COLUMN ${q(operation.column.name)}`;
|
|
285
|
+
case 'drop_column':
|
|
286
|
+
return refuseRecreateDroppedColumn(operation.table, operation.column);
|
|
287
|
+
case 'alter_column_type':
|
|
288
|
+
throw new UnsupportedFeatureError(
|
|
289
|
+
'alter column type',
|
|
290
|
+
'sqlite',
|
|
291
|
+
'sqlite cannot alter a column type in place; use a hand-written table rebuild',
|
|
292
|
+
);
|
|
293
|
+
case 'alter_primary_key':
|
|
294
|
+
return refuseAlterPrimaryKey(operation.table, operation.to, operation.from);
|
|
295
|
+
case 'add_foreign_key':
|
|
296
|
+
return refuseForeignKey('drop', operation.table, operation.fk);
|
|
297
|
+
case 'drop_foreign_key':
|
|
298
|
+
throw new UnsupportedFeatureError(
|
|
299
|
+
`recreating foreign key "${operation.name}" on "${operation.table}"`,
|
|
300
|
+
'sqlite',
|
|
301
|
+
`foreign key "${operation.name}" on "${operation.table}" cannot be recreated automatically because the ` +
|
|
302
|
+
'drop operation does not carry its columns or referential actions; write the down migration by hand',
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderIndexColumn(column: IndexColumn, definition: IndexDef): string {
|
|
308
|
+
if (typeof column === 'string') return q(column);
|
|
309
|
+
if (column.opclass !== undefined) {
|
|
310
|
+
throw new UnsupportedFeatureError(
|
|
311
|
+
`index operator class ${column.opclass}`,
|
|
312
|
+
'sqlite',
|
|
313
|
+
`sqlite does not support the index operator class ${column.opclass} ("${definition.name}")`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return 'expr' in column ? column.expr : q(column.column);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function createIndexDdl(definition: IndexDef): string {
|
|
320
|
+
if (definition.method !== undefined) {
|
|
321
|
+
throw new UnsupportedFeatureError(
|
|
322
|
+
`index method ${definition.method}`,
|
|
323
|
+
'sqlite',
|
|
324
|
+
`sqlite does not expose selectable index methods ("${definition.name}" on "${definition.table}")`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (definition.with !== undefined && Object.keys(definition.with).length > 0) {
|
|
328
|
+
throw new UnsupportedFeatureError(
|
|
329
|
+
`index options on "${definition.name}"`,
|
|
330
|
+
'sqlite',
|
|
331
|
+
`sqlite does not expose per-index storage options ("${definition.name}" on "${definition.table}")`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const unique = definition.unique === true ? 'UNIQUE ' : '';
|
|
335
|
+
const columns = definition.columns.map(column => renderIndexColumn(column, definition)).join(', ');
|
|
336
|
+
const where = definition.where === undefined ? '' : ` WHERE ${definition.where}`;
|
|
337
|
+
return `CREATE ${unique}INDEX ${q(definition.name)} ON ${q(definition.table)} (${columns})${where}`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function routineLabel(definition: RoutineDef): string {
|
|
341
|
+
return `${definition.kind} ${q(definition.name)}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function refuseRoutine(definition: RoutineDef): never {
|
|
345
|
+
const message =
|
|
346
|
+
`sqlite does not support stored routines (${routineLabel(definition)}); SQLite has no CREATE FUNCTION, ` +
|
|
347
|
+
'so register the function on the connection instead — `node:sqlite` exposes `DatabaseSync#function` — ' +
|
|
348
|
+
'and call it like any other';
|
|
349
|
+
throw new UnsupportedFeatureError(`stored routine ${routineLabel(definition)}`, 'sqlite', message);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function emitSchemaObject(operation: SchemaObjectOperation): readonly string[] {
|
|
353
|
+
switch (operation.kind) {
|
|
354
|
+
case 'create_index':
|
|
355
|
+
return [createIndexDdl(operation.definition)];
|
|
356
|
+
case 'check_constraint':
|
|
357
|
+
throw new UnsupportedFeatureError(
|
|
358
|
+
`adding check constraint "${operation.name}" on "${operation.table}"`,
|
|
359
|
+
'sqlite',
|
|
360
|
+
`sqlite cannot add check constraint "${operation.name}" to "${operation.table}" in place; ` +
|
|
361
|
+
'use a hand-written table rebuild',
|
|
362
|
+
);
|
|
363
|
+
case 'create_view':
|
|
364
|
+
if (operation.definition.materialized === true) {
|
|
365
|
+
throw new UnsupportedFeatureError('materialized views', 'sqlite');
|
|
366
|
+
}
|
|
367
|
+
return [`CREATE VIEW ${q(operation.definition.name)} AS ${operation.definition.select}`];
|
|
368
|
+
case 'drop_view':
|
|
369
|
+
if (operation.materialized === true) {
|
|
370
|
+
throw new UnsupportedFeatureError('materialized views', 'sqlite');
|
|
371
|
+
}
|
|
372
|
+
return [`DROP VIEW IF EXISTS ${q(operation.name)}`];
|
|
373
|
+
case 'create_sequence':
|
|
374
|
+
throw new UnsupportedFeatureError('sequences', 'sqlite');
|
|
375
|
+
case 'generated_column': {
|
|
376
|
+
const column = operation.definition;
|
|
377
|
+
return [
|
|
378
|
+
`${q(column.name)} ${column.type} GENERATED ALWAYS AS (${column.expression})${column.stored === true ? ' STORED' : ''}`,
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
case 'create_schema':
|
|
382
|
+
throw new UnsupportedFeatureError('schemas', 'sqlite');
|
|
383
|
+
case 'enable_rls':
|
|
384
|
+
case 'create_policy':
|
|
385
|
+
throw new UnsupportedFeatureError('row-level security', 'sqlite');
|
|
386
|
+
case 'create_extension':
|
|
387
|
+
throw new UnsupportedFeatureError(
|
|
388
|
+
`extension "${operation.definition.name}"`,
|
|
389
|
+
'sqlite',
|
|
390
|
+
`sqlite does not support database extensions ("${operation.definition.name}")`,
|
|
391
|
+
);
|
|
392
|
+
case 'create_routine':
|
|
393
|
+
case 'drop_routine':
|
|
394
|
+
return refuseRoutine(operation.definition);
|
|
395
|
+
case 'replace_routine':
|
|
396
|
+
return refuseRoutine(operation.next);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function parseAppliedMigrations(rows: readonly Record<string, unknown>[]): readonly AppliedMigration[] {
|
|
401
|
+
return rows.map((row, index) => {
|
|
402
|
+
const version = row.version;
|
|
403
|
+
const name = row.name;
|
|
404
|
+
const checksum = row.checksum;
|
|
405
|
+
if (
|
|
406
|
+
(typeof version !== 'number' && typeof version !== 'bigint' && typeof version !== 'string') ||
|
|
407
|
+
typeof name !== 'string' ||
|
|
408
|
+
(checksum !== null && typeof checksum !== 'string')
|
|
409
|
+
) {
|
|
410
|
+
throw new TypeError(`migration ledger row ${String(index)} has an invalid version, name or checksum`);
|
|
411
|
+
}
|
|
412
|
+
const numericVersion = Number(version);
|
|
413
|
+
if (!Number.isSafeInteger(numericVersion)) {
|
|
414
|
+
throw new TypeError(`migration ledger row ${String(index)} version is not a safe integer`);
|
|
415
|
+
}
|
|
416
|
+
return { version: numericVersion, name, checksum };
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function migrationChecksum(sql: string): Promise<string> {
|
|
421
|
+
const bytes = new TextEncoder().encode(sql);
|
|
422
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', bytes));
|
|
423
|
+
const hex = Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
424
|
+
return `sha256:${hex}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function connection(
|
|
428
|
+
driver: MigrationDriver<'sqlite'>,
|
|
429
|
+
options: MigrationTableOptions = {},
|
|
430
|
+
): MigrationConnection<'sqlite'> {
|
|
431
|
+
if (options.schema !== undefined) {
|
|
432
|
+
throw new UnsupportedFeatureError(
|
|
433
|
+
`migration schema "${options.schema}"`,
|
|
434
|
+
'sqlite',
|
|
435
|
+
'sqlite has no database schemas; omit migrations.schema',
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
const table = q(options.table ?? '_zmdb_migrations');
|
|
439
|
+
const execute = (text: string, parameters: readonly unknown[] = []) => driver.execute({ text, parameters });
|
|
440
|
+
|
|
441
|
+
const appliedMigrations = async (): Promise<readonly AppliedMigration[]> =>
|
|
442
|
+
parseAppliedMigrations(await execute(`SELECT version, name, checksum FROM ${table} ORDER BY version`));
|
|
443
|
+
|
|
444
|
+
const adapter: MigrationConnection<'sqlite'> = {
|
|
445
|
+
name: 'sqlite',
|
|
446
|
+
transactionalDdl: true,
|
|
447
|
+
async exec(sql: string): Promise<void> {
|
|
448
|
+
await execute(sql);
|
|
449
|
+
},
|
|
450
|
+
async appliedVersions(): Promise<readonly number[]> {
|
|
451
|
+
return (await appliedMigrations()).map(row => row.version);
|
|
452
|
+
},
|
|
453
|
+
appliedMigrations,
|
|
454
|
+
async recordApplied(version: number, name: string, checksum?: string): Promise<void> {
|
|
455
|
+
await execute(`INSERT INTO ${table} (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)`, [
|
|
456
|
+
version,
|
|
457
|
+
name,
|
|
458
|
+
Date.now(),
|
|
459
|
+
checksum ?? null,
|
|
460
|
+
]);
|
|
461
|
+
},
|
|
462
|
+
async recordReverted(version: number): Promise<void> {
|
|
463
|
+
await execute(`DELETE FROM ${table} WHERE version = ?`, [version]);
|
|
464
|
+
},
|
|
465
|
+
async ensureVersionTable(): Promise<void> {
|
|
466
|
+
await execute(
|
|
467
|
+
`CREATE TABLE IF NOT EXISTS ${table} (` +
|
|
468
|
+
'version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at INTEGER NOT NULL, checksum TEXT)',
|
|
469
|
+
);
|
|
470
|
+
try {
|
|
471
|
+
await execute(`SELECT checksum FROM ${table} WHERE 1 = 0`);
|
|
472
|
+
} catch {
|
|
473
|
+
await execute(`ALTER TABLE ${table} ADD COLUMN checksum TEXT`);
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
checksum: migrationChecksum,
|
|
477
|
+
async transaction<Result>(run: (nested?: MigrationConnection<'sqlite'>) => Promise<Result>): Promise<Result> {
|
|
478
|
+
if (driver.transaction === undefined) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
'sqlite migrations require a transactional driver; the driver must pin every callback query to one database transaction',
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return driver.transaction(nestedDriver => run(connection(nestedDriver, options)));
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
return adapter;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export const sqliteMigrations: MigrationDialect<'sqlite'> = {
|
|
490
|
+
name: 'sqlite',
|
|
491
|
+
foreignKeyMode: 'inline',
|
|
492
|
+
embedded: true,
|
|
493
|
+
validateSnapshot,
|
|
494
|
+
validatePlan,
|
|
495
|
+
ddlType: sqliteDdlType,
|
|
496
|
+
emitUp,
|
|
497
|
+
emitDown,
|
|
498
|
+
emitSchemaObject,
|
|
499
|
+
connection,
|
|
500
|
+
};
|
package/src/node.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { sqliteDriver, type SqliteDatabase, type SqliteOptions, type SqliteStatement } from './driver.js';
|