@telorun/sqlite 0.2.1 → 0.4.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/dist/schema/enum-controller.d.ts +18 -0
- package/dist/schema/enum-controller.js +25 -0
- package/dist/schema/schema-controller.d.ts +3 -1
- package/dist/schema/schema-controller.js +2 -1
- package/dist/schema/sqlite-schema-driver.d.ts +58 -1
- package/dist/schema/sqlite-schema-driver.js +176 -9
- package/dist/schema/table-controller.js +21 -2
- package/package.json +7 -3
- package/src/schema/enum-controller.ts +35 -0
- package/src/schema/schema-controller.ts +5 -2
- package/src/schema/sqlite-schema-driver.ts +229 -8
- package/src/schema/table-controller.ts +28 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
+
import { type DeclaredEnum, type RawEnum } from "@telorun/sql";
|
|
3
|
+
/**
|
|
4
|
+
* `SQLite.Enum` — one declared set of permitted values.
|
|
5
|
+
*
|
|
6
|
+
* SQLite has no named types, so nothing in the database corresponds to this
|
|
7
|
+
* resource: the values are rendered as a `CHECK` on every column that references
|
|
8
|
+
* it. The DECLARATION is still a ledger object like any other, which is what
|
|
9
|
+
* lets a change to it be detected at all.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SqliteEnumResource implements ResourceInstance {
|
|
12
|
+
readonly declaration: DeclaredEnum;
|
|
13
|
+
constructor(raw: RawEnum);
|
|
14
|
+
get typeName(): string;
|
|
15
|
+
snapshot(): Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export declare function register(): void;
|
|
18
|
+
export declare function create(resource: RawEnum, _ctx: ResourceContext): Promise<SqliteEnumResource>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { normalizeEnum } from "@telorun/sql";
|
|
2
|
+
/**
|
|
3
|
+
* `SQLite.Enum` — one declared set of permitted values.
|
|
4
|
+
*
|
|
5
|
+
* SQLite has no named types, so nothing in the database corresponds to this
|
|
6
|
+
* resource: the values are rendered as a `CHECK` on every column that references
|
|
7
|
+
* it. The DECLARATION is still a ledger object like any other, which is what
|
|
8
|
+
* lets a change to it be detected at all.
|
|
9
|
+
*/
|
|
10
|
+
export class SqliteEnumResource {
|
|
11
|
+
declaration;
|
|
12
|
+
constructor(raw) {
|
|
13
|
+
this.declaration = normalizeEnum(raw);
|
|
14
|
+
}
|
|
15
|
+
get typeName() {
|
|
16
|
+
return this.declaration.typeName;
|
|
17
|
+
}
|
|
18
|
+
snapshot() {
|
|
19
|
+
return { typeName: this.declaration.typeName, values: [...this.declaration.values] };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function register() { }
|
|
23
|
+
export async function create(resource, _ctx) {
|
|
24
|
+
return new SqliteEnumResource(resource);
|
|
25
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import { type MigrationMap, type ReclaimPolicy, type SqlConnection } from "@telorun/sql";
|
|
3
|
+
import type { SqliteEnumResource } from "./enum-controller.js";
|
|
3
4
|
import type { SqliteTableResource } from "./table-controller.js";
|
|
4
5
|
interface SqliteSchemaManifest {
|
|
5
6
|
metadata: {
|
|
@@ -10,7 +11,8 @@ interface SqliteSchemaManifest {
|
|
|
10
11
|
version?: string;
|
|
11
12
|
ledger?: string;
|
|
12
13
|
tables?: SqliteTableResource[];
|
|
13
|
-
|
|
14
|
+
enums?: SqliteEnumResource[];
|
|
15
|
+
prepare?: MigrationMap;
|
|
14
16
|
migrations?: MigrationMap;
|
|
15
17
|
reclaim?: ReclaimPolicy;
|
|
16
18
|
}
|
|
@@ -31,7 +31,8 @@ class SqliteSchemaResource {
|
|
|
31
31
|
ledger: this.manifest.ledger,
|
|
32
32
|
version: this.manifest.version,
|
|
33
33
|
tables: (this.manifest.tables ?? []).map((table) => table.declaration),
|
|
34
|
-
|
|
34
|
+
enums: (this.manifest.enums ?? []).map((declared) => declared.declaration),
|
|
35
|
+
prepare: this.manifest.prepare ?? {},
|
|
35
36
|
migrations: this.manifest.migrations ?? {},
|
|
36
37
|
reclaim: this.manifest.reclaim,
|
|
37
38
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ChangeSafety, type DeclaredColumn, type DeclaredForeignKey, type DeclaredIndex, type DeclaredTable, type LiveColumn, type SchemaObjectId, type LiveTable, type LedgerTables, type LiveForeignKey, type SchemaDriver, type SqlConnection } from "@telorun/sql";
|
|
1
|
+
import { type ChangeSafety, type DeclaredCheck, type DeclaredColumn, type DeclaredEnum, type DeclaredForeignKey, type DeclaredIndex, type DeclaredTable, type LiveCheck, type LiveColumn, type LiveEnum, type SchemaObjectId, type LiveTable, type LedgerTables, type LiveForeignKey, type SchemaDriver, type SqlConnection } from "@telorun/sql";
|
|
2
2
|
/**
|
|
3
3
|
* SQLite's half of declarative schema.
|
|
4
4
|
*
|
|
@@ -44,6 +44,11 @@ export declare class SqliteSchemaDriver implements SchemaDriver {
|
|
|
44
44
|
ledgerStatements(_schema: string, tables: LedgerTables): string[];
|
|
45
45
|
now(): Promise<string>;
|
|
46
46
|
runAtomically(statements: readonly string[]): Promise<void>;
|
|
47
|
+
/** SQLite has no DDL this design must keep out of a transaction, so this is
|
|
48
|
+
* `runAtomically` under the name the contract asks for. */
|
|
49
|
+
runSequentially(statements: readonly string[]): Promise<void>;
|
|
50
|
+
/** All of them: SQLite has no DDL this design must keep out of a transaction. */
|
|
51
|
+
transactionalPhase(): boolean;
|
|
47
52
|
introspect(_schema: string, tables: readonly string[]): Promise<LiveTable[]>;
|
|
48
53
|
typeSignature(column: DeclaredColumn): string;
|
|
49
54
|
/** An index is dropped and recreated, which SQLite does support. */
|
|
@@ -70,7 +75,59 @@ export declare class SqliteSchemaDriver implements SchemaDriver {
|
|
|
70
75
|
addForeignKey(_schema: string, table: string, fk: DeclaredForeignKey): string[];
|
|
71
76
|
/** Unreachable: `canReclaim` refuses a foreign key before the drop is planned. */
|
|
72
77
|
dropForeignKey(_schema: string, table: string, name: string): string[];
|
|
78
|
+
/** SQLite has no `ADD CONSTRAINT`, so a check exists only as part of the table
|
|
79
|
+
* it was created with — the same place its foreign keys are. */
|
|
80
|
+
readonly checksInCreateTable = true;
|
|
81
|
+
/** Compared against the RECORDED declaration rather than live state, so this
|
|
82
|
+
* is declaration against declaration and the text is the whole comparison. */
|
|
83
|
+
checkDiffers(live: LiveCheck, declared: DeclaredCheck): boolean;
|
|
84
|
+
classifyCheckChange(_live: LiveCheck, declared: DeclaredCheck): ChangeSafety;
|
|
85
|
+
addCheck(_schema: string, table: string, check: DeclaredCheck): string[];
|
|
86
|
+
dropCheck(_schema: string, table: string, name: string): string[];
|
|
87
|
+
/** Unreachable: nothing on SQLite is ever added `NOT VALID`, so nothing is
|
|
88
|
+
* waiting to be proven. */
|
|
89
|
+
validateCheck(_schema: string, table: string, name: string): string[];
|
|
90
|
+
/**
|
|
91
|
+
* SQLite has no named types, so a domain has no database object behind it: the
|
|
92
|
+
* values are rendered as a `CHECK` on every column that references the enum.
|
|
93
|
+
*
|
|
94
|
+
* The DECLARATION is still a ledger object like any other, which is what lets a
|
|
95
|
+
* change to it be detected at all — see `classifyEnumChange`.
|
|
96
|
+
*/
|
|
97
|
+
readonly namedEnumTypes = false;
|
|
98
|
+
/** Nothing to read back: there is no type to introspect. */
|
|
99
|
+
introspectEnums(): Promise<LiveEnum[]>;
|
|
100
|
+
/** Unreachable: the shared half asks only when `namedEnumTypes` holds. */
|
|
101
|
+
createEnum(_schema: string, declared: DeclaredEnum): string[];
|
|
102
|
+
addEnumValues(_schema: string, declared: DeclaredEnum): string[];
|
|
103
|
+
/** No statement at all — the `CHECK`s on referencing tables never named the
|
|
104
|
+
* type, so the ledger key rewrite IS the rename. */
|
|
105
|
+
renameEnum(): string[];
|
|
106
|
+
/** The one part of renaming where the two engines need no separate story:
|
|
107
|
+
* SQLite renames a table with the same statement PostgreSQL does. */
|
|
108
|
+
renameTable(schema: string, from: string, to: string): string[];
|
|
109
|
+
/**
|
|
110
|
+
* A domain reaches the database inside the tables that use it, so changing one
|
|
111
|
+
* would mean rebuilding every referencing table — which is where SQLite's
|
|
112
|
+
* foreign keys and column alterations already are.
|
|
113
|
+
*
|
|
114
|
+
* The comparison is against the RECORDED declaration rather than live state,
|
|
115
|
+
* because there is no live state: nothing in the database corresponds to the
|
|
116
|
+
* type. That is exactly what makes snapshotting the whole enum declaration
|
|
117
|
+
* load-bearing rather than decorative.
|
|
118
|
+
*/
|
|
119
|
+
classifyEnumChange(_live: LiveEnum | undefined, declared: DeclaredEnum, owned: DeclaredEnum | undefined): ChangeSafety;
|
|
120
|
+
dropEnum(): string[];
|
|
121
|
+
/** SQLite has no installable extensions in the sense this declares — a
|
|
122
|
+
* loadable extension is a build-time or connection-time concern, not a schema
|
|
123
|
+
* object a pass can create. */
|
|
124
|
+
readonly namedExtensions = false;
|
|
125
|
+
introspectExtensions(): Promise<string[]>;
|
|
126
|
+
createExtension(_schema: string, name: string): string[];
|
|
127
|
+
dropExtension(_schema: string, name: string): string[];
|
|
73
128
|
canReclaim(id: SchemaObjectId): ChangeSafety;
|
|
74
129
|
dropColumn(schema: string, table: string, column: string): string[];
|
|
75
130
|
dropTable(schema: string, table: string): string[];
|
|
131
|
+
upsertRow(schema: string, table: string, key: readonly string[], row: Record<string, unknown>): string[];
|
|
132
|
+
deleteRow(schema: string, table: string, key: readonly string[], row: Record<string, unknown>): string[];
|
|
76
133
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { quoteAnsiIdentifier, } from "@telorun/sql";
|
|
1
|
+
import { quoteAnsiIdentifier, deleteRowStatements, upsertRowStatements, } from "@telorun/sql";
|
|
2
2
|
import { CompiledQuery } from "kysely";
|
|
3
3
|
/**
|
|
4
4
|
* SQLite's half of declarative schema.
|
|
@@ -14,14 +14,36 @@ import { CompiledQuery } from "kysely";
|
|
|
14
14
|
* conventions over these five, and inventing names for them here would be the
|
|
15
15
|
* lowest-common-denominator type vocabulary this design rejects. */
|
|
16
16
|
export const SQLITE_TYPES = ["integer", "real", "text", "blob", "numeric"];
|
|
17
|
+
/**
|
|
18
|
+
* A declared value as SQLite literal text.
|
|
19
|
+
*
|
|
20
|
+
* Structured values are SERIALIZED, never stringified. `String({})` is
|
|
21
|
+
* `[object Object]` and `String([1,2])` is `1,2`, so a structured seed value —
|
|
22
|
+
* which the row projection admits wherever a column's mapped node is open —
|
|
23
|
+
* reached the database as that text. SQLite has no JSON type, only the
|
|
24
|
+
* convention of JSON in a `text` column, and the serialized form is exactly what
|
|
25
|
+
* that convention (and `json_extract`) reads.
|
|
26
|
+
*
|
|
27
|
+
* Bytes get SQLite's own blob literal: `!include-bytes` resolves to a
|
|
28
|
+
* `Uint8Array` before a controller sees it, and `String(...)` on one yields its
|
|
29
|
+
* elements comma-joined.
|
|
30
|
+
*/
|
|
17
31
|
function literal(value) {
|
|
18
|
-
if (value === null)
|
|
32
|
+
if (value === null || value === undefined)
|
|
19
33
|
return "NULL";
|
|
20
34
|
if (typeof value === "number" || typeof value === "bigint")
|
|
21
35
|
return String(value);
|
|
22
36
|
if (typeof value === "boolean")
|
|
23
37
|
return value ? "1" : "0";
|
|
24
|
-
|
|
38
|
+
if (value instanceof Uint8Array)
|
|
39
|
+
return `X'${Buffer.from(value).toString("hex")}'`;
|
|
40
|
+
if (typeof value === "object")
|
|
41
|
+
return quoted(JSON.stringify(value));
|
|
42
|
+
return quoted(String(value));
|
|
43
|
+
}
|
|
44
|
+
/** Single quotes doubled — SQLite's only string escape. */
|
|
45
|
+
function quoted(text) {
|
|
46
|
+
return `'${text.replace(/'/g, "''")}'`;
|
|
25
47
|
}
|
|
26
48
|
function columnDefault(column) {
|
|
27
49
|
if (column.defaultExpression !== undefined)
|
|
@@ -100,6 +122,15 @@ export class SqliteSchemaDriver {
|
|
|
100
122
|
}
|
|
101
123
|
});
|
|
102
124
|
}
|
|
125
|
+
/** SQLite has no DDL this design must keep out of a transaction, so this is
|
|
126
|
+
* `runAtomically` under the name the contract asks for. */
|
|
127
|
+
async runSequentially(statements) {
|
|
128
|
+
await this.runAtomically(statements);
|
|
129
|
+
}
|
|
130
|
+
/** All of them: SQLite has no DDL this design must keep out of a transaction. */
|
|
131
|
+
transactionalPhase() {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
103
134
|
async introspect(_schema, tables) {
|
|
104
135
|
const live = [];
|
|
105
136
|
for (const table of tables) {
|
|
@@ -161,7 +192,11 @@ export class SqliteSchemaDriver {
|
|
|
161
192
|
onUpdate: first.on_update == null ? undefined : String(first.on_update),
|
|
162
193
|
});
|
|
163
194
|
}
|
|
164
|
-
|
|
195
|
+
// Checks are deliberately NOT read back: SQLite keeps them only inside the
|
|
196
|
+
// table's stored DDL text, and parsing that would be a SQL parser in a
|
|
197
|
+
// schema driver. The shared half compares this engine's checks against the
|
|
198
|
+
// RECORDED declaration instead — see `checksInCreateTable`.
|
|
199
|
+
live.push({ name: table, columns, indexes, foreignKeys, checks: [] });
|
|
165
200
|
}
|
|
166
201
|
return live;
|
|
167
202
|
}
|
|
@@ -243,6 +278,14 @@ export class SqliteSchemaDriver {
|
|
|
243
278
|
parts.push("NOT NULL");
|
|
244
279
|
if (column.unique)
|
|
245
280
|
parts.push("UNIQUE");
|
|
281
|
+
// SQLite has no named types, so a domain reaches the database only as a
|
|
282
|
+
// constraint on the column that uses it. The values sort as text rather than
|
|
283
|
+
// in declaration order — the one thing the rendering cannot reproduce, and
|
|
284
|
+
// why the enum's `values:` order is documented as PostgreSQL's alone.
|
|
285
|
+
if (column.enum) {
|
|
286
|
+
const values = column.enum.values.map((value) => literal(value)).join(", ");
|
|
287
|
+
parts.push(`CHECK (${this.quote(column.name)} IN (${values}))`);
|
|
288
|
+
}
|
|
246
289
|
const def = columnDefault(column);
|
|
247
290
|
return parts.join(" ") + def;
|
|
248
291
|
}
|
|
@@ -255,10 +298,14 @@ export class SqliteSchemaDriver {
|
|
|
255
298
|
createTable(schema, table) {
|
|
256
299
|
const parts = table.columns.map((column) => this.#columnDefinition(column));
|
|
257
300
|
// Foreign keys are part of the table in SQLite — there is no ADD CONSTRAINT
|
|
258
|
-
// — so they are emitted here and nowhere else.
|
|
301
|
+
// — so they are emitted here and nowhere else. Named checks land in exactly
|
|
302
|
+
// the same place, for exactly the same reason.
|
|
259
303
|
for (const fk of table.foreignKeys) {
|
|
260
304
|
parts.push(this.#foreignKeyClause(fk));
|
|
261
305
|
}
|
|
306
|
+
for (const check of table.checks) {
|
|
307
|
+
parts.push(`CONSTRAINT ${this.quote(check.name)} CHECK (${check.expression})`);
|
|
308
|
+
}
|
|
262
309
|
return [
|
|
263
310
|
`CREATE TABLE IF NOT EXISTS ${this.qualify(schema, table.name)} (\n ${parts.join(",\n ")}\n)`,
|
|
264
311
|
];
|
|
@@ -316,13 +363,116 @@ export class SqliteSchemaDriver {
|
|
|
316
363
|
throw new Error(`SQLite.Table: foreign key '${name}' cannot be dropped from '${table}' — SQLite has no ` +
|
|
317
364
|
`DROP CONSTRAINT. Rebuild the table in a 'migrations:' entry.`);
|
|
318
365
|
}
|
|
366
|
+
/** SQLite has no `ADD CONSTRAINT`, so a check exists only as part of the table
|
|
367
|
+
* it was created with — the same place its foreign keys are. */
|
|
368
|
+
checksInCreateTable = true;
|
|
369
|
+
/** Compared against the RECORDED declaration rather than live state, so this
|
|
370
|
+
* is declaration against declaration and the text is the whole comparison. */
|
|
371
|
+
checkDiffers(live, declared) {
|
|
372
|
+
return live.expression !== declared.expression;
|
|
373
|
+
}
|
|
374
|
+
classifyCheckChange(_live, declared) {
|
|
375
|
+
return {
|
|
376
|
+
safe: false,
|
|
377
|
+
reason: `SQLite has no ALTER for a constraint — a check exists only as part of the table it was ` +
|
|
378
|
+
`created with — so '${declared.name}' cannot be changed in place. Rebuild the table in a ` +
|
|
379
|
+
`'migrations:' entry.`,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
addCheck(_schema, table, check) {
|
|
383
|
+
throw new Error(`SQLite.Table: check '${check.name}' cannot be added to the existing table '${table}' — ` +
|
|
384
|
+
`SQLite has no ADD CONSTRAINT and a check exists only as part of the table it was ` +
|
|
385
|
+
`created with. Rebuild the table in a 'migrations:' entry.`);
|
|
386
|
+
}
|
|
387
|
+
dropCheck(_schema, table, name) {
|
|
388
|
+
throw new Error(`SQLite.Table: check '${name}' cannot be dropped from '${table}' — SQLite has no DROP ` +
|
|
389
|
+
`CONSTRAINT. Rebuild the table in a 'migrations:' entry.`);
|
|
390
|
+
}
|
|
391
|
+
/** Unreachable: nothing on SQLite is ever added `NOT VALID`, so nothing is
|
|
392
|
+
* waiting to be proven. */
|
|
393
|
+
validateCheck(_schema, table, name) {
|
|
394
|
+
throw new Error(`SQLite.Table: check '${table}.${name}' has no deferred validation to run.`);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* SQLite has no named types, so a domain has no database object behind it: the
|
|
398
|
+
* values are rendered as a `CHECK` on every column that references the enum.
|
|
399
|
+
*
|
|
400
|
+
* The DECLARATION is still a ledger object like any other, which is what lets a
|
|
401
|
+
* change to it be detected at all — see `classifyEnumChange`.
|
|
402
|
+
*/
|
|
403
|
+
namedEnumTypes = false;
|
|
404
|
+
/** Nothing to read back: there is no type to introspect. */
|
|
405
|
+
async introspectEnums() {
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
/** Unreachable: the shared half asks only when `namedEnumTypes` holds. */
|
|
409
|
+
createEnum(_schema, declared) {
|
|
410
|
+
throw new Error(`SQLite.Enum '${declared.typeName}': SQLite has no named types, so nothing creates one.`);
|
|
411
|
+
}
|
|
412
|
+
addEnumValues(_schema, declared) {
|
|
413
|
+
throw new Error(`SQLite.Enum '${declared.typeName}': SQLite has no named types, so nothing alters one.`);
|
|
414
|
+
}
|
|
415
|
+
/** No statement at all — the `CHECK`s on referencing tables never named the
|
|
416
|
+
* type, so the ledger key rewrite IS the rename. */
|
|
417
|
+
renameEnum() {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
/** The one part of renaming where the two engines need no separate story:
|
|
421
|
+
* SQLite renames a table with the same statement PostgreSQL does. */
|
|
422
|
+
renameTable(schema, from, to) {
|
|
423
|
+
return [`ALTER TABLE ${this.qualify(schema, from)} RENAME TO ${this.quote(to)}`];
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* A domain reaches the database inside the tables that use it, so changing one
|
|
427
|
+
* would mean rebuilding every referencing table — which is where SQLite's
|
|
428
|
+
* foreign keys and column alterations already are.
|
|
429
|
+
*
|
|
430
|
+
* The comparison is against the RECORDED declaration rather than live state,
|
|
431
|
+
* because there is no live state: nothing in the database corresponds to the
|
|
432
|
+
* type. That is exactly what makes snapshotting the whole enum declaration
|
|
433
|
+
* load-bearing rather than decorative.
|
|
434
|
+
*/
|
|
435
|
+
classifyEnumChange(_live, declared, owned) {
|
|
436
|
+
if (!owned)
|
|
437
|
+
return { safe: true };
|
|
438
|
+
const same = owned.baseType === declared.baseType &&
|
|
439
|
+
owned.values.length === declared.values.length &&
|
|
440
|
+
owned.values.every((value, i) => value === declared.values[i]);
|
|
441
|
+
if (same)
|
|
442
|
+
return { safe: true };
|
|
443
|
+
return {
|
|
444
|
+
safe: false,
|
|
445
|
+
reason: `SQLite has no named types, so '${declared.typeName}' is rendered as a CHECK on every ` +
|
|
446
|
+
`column that references it — and SQLite has no ALTER for a constraint. Changing the ` +
|
|
447
|
+
`declaration would leave every existing table enforcing the old values. Rebuild the ` +
|
|
448
|
+
`referencing tables in a 'migrations:' entry, which is where a changed constraint on ` +
|
|
449
|
+
`this engine belongs.`,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
dropEnum() {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
/** SQLite has no installable extensions in the sense this declares — a
|
|
456
|
+
* loadable extension is a build-time or connection-time concern, not a schema
|
|
457
|
+
* object a pass can create. */
|
|
458
|
+
namedExtensions = false;
|
|
459
|
+
async introspectExtensions() {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
createExtension(_schema, name) {
|
|
463
|
+
throw new Error(`SQLite.Schema: extension '${name}' cannot be created — SQLite has no installable ` +
|
|
464
|
+
`extensions a schema pass can provision.`);
|
|
465
|
+
}
|
|
466
|
+
dropExtension(_schema, name) {
|
|
467
|
+
throw new Error(`SQLite.Schema: extension '${name}' cannot be dropped.`);
|
|
468
|
+
}
|
|
319
469
|
canReclaim(id) {
|
|
320
|
-
if (id.kind === "foreignKey") {
|
|
470
|
+
if (id.kind === "foreignKey" || id.kind === "check") {
|
|
321
471
|
return {
|
|
322
472
|
safe: false,
|
|
323
|
-
reason:
|
|
324
|
-
"Rebuild the table in a 'migrations:' entry; the tombstone
|
|
325
|
-
"constraint is gone.",
|
|
473
|
+
reason: `SQLite has no DROP CONSTRAINT, so this ${id.kind === "check" ? "check" : "foreign key"} ` +
|
|
474
|
+
"cannot be dropped in place. Rebuild the table in a 'migrations:' entry; the tombstone " +
|
|
475
|
+
"is cleared when the constraint is gone.",
|
|
326
476
|
};
|
|
327
477
|
}
|
|
328
478
|
return { safe: true };
|
|
@@ -333,4 +483,21 @@ export class SqliteSchemaDriver {
|
|
|
333
483
|
dropTable(schema, table) {
|
|
334
484
|
return [`DROP TABLE IF EXISTS ${this.qualify(schema, table)}`];
|
|
335
485
|
}
|
|
486
|
+
/** SQLite has had upsert since 3.24, so both engines render the same statement
|
|
487
|
+
* SHAPE — which is why it lives in `row-statements.ts` and only the dialect
|
|
488
|
+
* is answered here. */
|
|
489
|
+
get #rowDialect() {
|
|
490
|
+
return {
|
|
491
|
+
quote: (name) => this.quote(name),
|
|
492
|
+
literal,
|
|
493
|
+
qualify: (schema, table) => this.qualify(schema, table),
|
|
494
|
+
excludedAlias: "excluded",
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
upsertRow(schema, table, key, row) {
|
|
498
|
+
return upsertRowStatements(this.#rowDialect, schema, table, key, row);
|
|
499
|
+
}
|
|
500
|
+
deleteRow(schema, table, key, row) {
|
|
501
|
+
return deleteRowStatements(this.#rowDialect, schema, table, key, row);
|
|
502
|
+
}
|
|
336
503
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeTable, tableReferenceResolver, } from "@telorun/sql";
|
|
1
|
+
import { enumReferenceResolver, normalizeTable, tableReferenceResolver, } from "@telorun/sql";
|
|
2
2
|
/**
|
|
3
3
|
* `SQLite.Table` — one physical table, declared rather than migrated to.
|
|
4
4
|
*
|
|
@@ -9,7 +9,7 @@ import { normalizeTable, tableReferenceResolver, } from "@telorun/sql";
|
|
|
9
9
|
export class SqliteTableResource {
|
|
10
10
|
declaration;
|
|
11
11
|
constructor(raw, ctx) {
|
|
12
|
-
this.declaration = normalizeTable(raw, tableReferenceResolver(ctx, "SQLite.Table", raw.table));
|
|
12
|
+
this.declaration = normalizeTable(raw, tableReferenceResolver(ctx, "SQLite.Table", raw.table), sqliteColumnType(ctx, raw.table));
|
|
13
13
|
}
|
|
14
14
|
/** The physical table name, read by consumers that build statements against
|
|
15
15
|
* it (`self.table.table` in a repository's template). */
|
|
@@ -20,6 +20,25 @@ export class SqliteTableResource {
|
|
|
20
20
|
return { table: this.declaration.name };
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/** SQLite has no named types, so an enum column's engine-native type is the
|
|
24
|
+
* enum's declared base storage class; the values ride along as a CHECK. */
|
|
25
|
+
function sqliteColumnType(ctx, table) {
|
|
26
|
+
const resolveEnum = enumReferenceResolver(ctx, "SQLite.Enum", table);
|
|
27
|
+
return (value, column) => {
|
|
28
|
+
const declared = resolveEnum(value, column);
|
|
29
|
+
if (!declared)
|
|
30
|
+
return { type: String(value) };
|
|
31
|
+
if (!declared.baseType) {
|
|
32
|
+
throw new Error(`SQLite.Table '${table}': column '${column}' references enum '${declared.typeName}', ` +
|
|
33
|
+
`which declares no 'baseType'. SQLite has no named types, so something has to say ` +
|
|
34
|
+
`which storage class the values sit in.`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
type: declared.baseType,
|
|
38
|
+
enum: { typeName: declared.typeName, values: declared.values },
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
23
42
|
export function register() { }
|
|
24
43
|
export async function create(resource, ctx) {
|
|
25
44
|
return new SqliteTableResource(resource, ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/sqlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Telo SQLite.Connection — SQLite backend for the Sql.Connection abstract (better-sqlite3 / bun:sqlite, transactional DDL).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"./table": {
|
|
36
36
|
"bun": "./src/schema/table-controller.ts",
|
|
37
37
|
"import": "./dist/schema/table-controller.js"
|
|
38
|
+
},
|
|
39
|
+
"./enum": {
|
|
40
|
+
"bun": "./src/schema/enum-controller.ts",
|
|
41
|
+
"import": "./dist/schema/enum-controller.js"
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
44
|
"files": [
|
|
@@ -44,14 +48,14 @@
|
|
|
44
48
|
"dependencies": {
|
|
45
49
|
"better-sqlite3": "^12.8.0",
|
|
46
50
|
"kysely": "^0.28.15",
|
|
47
|
-
"@telorun/sql": "0.
|
|
51
|
+
"@telorun/sql": "0.24.0"
|
|
48
52
|
},
|
|
49
53
|
"devDependencies": {
|
|
50
54
|
"@types/better-sqlite3": "^7.0.0",
|
|
51
55
|
"@types/bun": "^1.3.10",
|
|
52
56
|
"@types/node": "^20.0.0",
|
|
53
57
|
"typescript": "^5.0.0",
|
|
54
|
-
"@telorun/sdk": "0.
|
|
58
|
+
"@telorun/sdk": "0.82.1"
|
|
55
59
|
},
|
|
56
60
|
"peerDependencies": {
|
|
57
61
|
"@telorun/sdk": "*"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
+
import { normalizeEnum, type DeclaredEnum, type RawEnum } from "@telorun/sql";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `SQLite.Enum` — one declared set of permitted values.
|
|
6
|
+
*
|
|
7
|
+
* SQLite has no named types, so nothing in the database corresponds to this
|
|
8
|
+
* resource: the values are rendered as a `CHECK` on every column that references
|
|
9
|
+
* it. The DECLARATION is still a ledger object like any other, which is what
|
|
10
|
+
* lets a change to it be detected at all.
|
|
11
|
+
*/
|
|
12
|
+
export class SqliteEnumResource implements ResourceInstance {
|
|
13
|
+
readonly declaration: DeclaredEnum;
|
|
14
|
+
|
|
15
|
+
constructor(raw: RawEnum) {
|
|
16
|
+
this.declaration = normalizeEnum(raw);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get typeName(): string {
|
|
20
|
+
return this.declaration.typeName;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
snapshot(): Record<string, unknown> {
|
|
24
|
+
return { typeName: this.declaration.typeName, values: [...this.declaration.values] };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function register(): void {}
|
|
29
|
+
|
|
30
|
+
export async function create(
|
|
31
|
+
resource: RawEnum,
|
|
32
|
+
_ctx: ResourceContext,
|
|
33
|
+
): Promise<SqliteEnumResource> {
|
|
34
|
+
return new SqliteEnumResource(resource);
|
|
35
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type SqlConnection,
|
|
8
8
|
} from "@telorun/sql";
|
|
9
9
|
import { SqliteSchemaDriver } from "./sqlite-schema-driver.js";
|
|
10
|
+
import type { SqliteEnumResource } from "./enum-controller.js";
|
|
10
11
|
import type { SqliteTableResource } from "./table-controller.js";
|
|
11
12
|
|
|
12
13
|
interface SqliteSchemaManifest {
|
|
@@ -15,7 +16,8 @@ interface SqliteSchemaManifest {
|
|
|
15
16
|
version?: string;
|
|
16
17
|
ledger?: string;
|
|
17
18
|
tables?: SqliteTableResource[];
|
|
18
|
-
|
|
19
|
+
enums?: SqliteEnumResource[];
|
|
20
|
+
prepare?: MigrationMap;
|
|
19
21
|
migrations?: MigrationMap;
|
|
20
22
|
reclaim?: ReclaimPolicy;
|
|
21
23
|
}
|
|
@@ -56,7 +58,8 @@ class SqliteSchemaResource implements ResourceInstance {
|
|
|
56
58
|
ledger: this.manifest.ledger,
|
|
57
59
|
version: this.manifest.version,
|
|
58
60
|
tables: (this.manifest.tables ?? []).map((table) => table.declaration),
|
|
59
|
-
|
|
61
|
+
enums: (this.manifest.enums ?? []).map((declared) => declared.declaration),
|
|
62
|
+
prepare: this.manifest.prepare ?? {},
|
|
60
63
|
migrations: this.manifest.migrations ?? {},
|
|
61
64
|
reclaim: this.manifest.reclaim,
|
|
62
65
|
});
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
quoteAnsiIdentifier,
|
|
3
3
|
type ChangeSafety,
|
|
4
|
+
type DeclaredCheck,
|
|
4
5
|
type DeclaredColumn,
|
|
6
|
+
type DeclaredEnum,
|
|
5
7
|
type DeclaredForeignKey,
|
|
6
8
|
type DeclaredIndex,
|
|
7
9
|
type DeclaredTable,
|
|
10
|
+
type LiveCheck,
|
|
8
11
|
type LiveColumn,
|
|
12
|
+
type LiveEnum,
|
|
9
13
|
type SchemaObjectId,
|
|
10
14
|
type LiveTable,
|
|
11
15
|
type LedgerTables,
|
|
@@ -13,6 +17,9 @@ import {
|
|
|
13
17
|
type LiveIndex,
|
|
14
18
|
type SchemaDriver,
|
|
15
19
|
type SqlConnection,
|
|
20
|
+
deleteRowStatements,
|
|
21
|
+
upsertRowStatements,
|
|
22
|
+
type RowDialect,
|
|
16
23
|
} from "@telorun/sql";
|
|
17
24
|
import { CompiledQuery, type Kysely } from "kysely";
|
|
18
25
|
|
|
@@ -33,11 +40,32 @@ import { CompiledQuery, type Kysely } from "kysely";
|
|
|
33
40
|
export const SQLITE_TYPES = ["integer", "real", "text", "blob", "numeric"] as const;
|
|
34
41
|
export type SqliteType = (typeof SQLITE_TYPES)[number];
|
|
35
42
|
|
|
43
|
+
/**
|
|
44
|
+
* A declared value as SQLite literal text.
|
|
45
|
+
*
|
|
46
|
+
* Structured values are SERIALIZED, never stringified. `String({})` is
|
|
47
|
+
* `[object Object]` and `String([1,2])` is `1,2`, so a structured seed value —
|
|
48
|
+
* which the row projection admits wherever a column's mapped node is open —
|
|
49
|
+
* reached the database as that text. SQLite has no JSON type, only the
|
|
50
|
+
* convention of JSON in a `text` column, and the serialized form is exactly what
|
|
51
|
+
* that convention (and `json_extract`) reads.
|
|
52
|
+
*
|
|
53
|
+
* Bytes get SQLite's own blob literal: `!include-bytes` resolves to a
|
|
54
|
+
* `Uint8Array` before a controller sees it, and `String(...)` on one yields its
|
|
55
|
+
* elements comma-joined.
|
|
56
|
+
*/
|
|
36
57
|
function literal(value: unknown): string {
|
|
37
|
-
if (value === null) return "NULL";
|
|
58
|
+
if (value === null || value === undefined) return "NULL";
|
|
38
59
|
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
39
60
|
if (typeof value === "boolean") return value ? "1" : "0";
|
|
40
|
-
return `'${
|
|
61
|
+
if (value instanceof Uint8Array) return `X'${Buffer.from(value).toString("hex")}'`;
|
|
62
|
+
if (typeof value === "object") return quoted(JSON.stringify(value));
|
|
63
|
+
return quoted(String(value));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Single quotes doubled — SQLite's only string escape. */
|
|
67
|
+
function quoted(text: string): string {
|
|
68
|
+
return `'${text.replace(/'/g, "''")}'`;
|
|
41
69
|
}
|
|
42
70
|
|
|
43
71
|
function columnDefault(column: DeclaredColumn): string {
|
|
@@ -125,6 +153,17 @@ export class SqliteSchemaDriver implements SchemaDriver {
|
|
|
125
153
|
});
|
|
126
154
|
}
|
|
127
155
|
|
|
156
|
+
/** SQLite has no DDL this design must keep out of a transaction, so this is
|
|
157
|
+
* `runAtomically` under the name the contract asks for. */
|
|
158
|
+
async runSequentially(statements: readonly string[]): Promise<void> {
|
|
159
|
+
await this.runAtomically(statements);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** All of them: SQLite has no DDL this design must keep out of a transaction. */
|
|
163
|
+
transactionalPhase(): boolean {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
128
167
|
async introspect(_schema: string, tables: readonly string[]): Promise<LiveTable[]> {
|
|
129
168
|
const live: LiveTable[] = [];
|
|
130
169
|
for (const table of tables) {
|
|
@@ -195,7 +234,11 @@ export class SqliteSchemaDriver implements SchemaDriver {
|
|
|
195
234
|
});
|
|
196
235
|
}
|
|
197
236
|
|
|
198
|
-
|
|
237
|
+
// Checks are deliberately NOT read back: SQLite keeps them only inside the
|
|
238
|
+
// table's stored DDL text, and parsing that would be a SQL parser in a
|
|
239
|
+
// schema driver. The shared half compares this engine's checks against the
|
|
240
|
+
// RECORDED declaration instead — see `checksInCreateTable`.
|
|
241
|
+
live.push({ name: table, columns, indexes, foreignKeys, checks: [] });
|
|
199
242
|
}
|
|
200
243
|
return live;
|
|
201
244
|
}
|
|
@@ -288,6 +331,14 @@ export class SqliteSchemaDriver implements SchemaDriver {
|
|
|
288
331
|
if (column.identity) parts.push("AUTOINCREMENT");
|
|
289
332
|
if (!column.nullable) parts.push("NOT NULL");
|
|
290
333
|
if (column.unique) parts.push("UNIQUE");
|
|
334
|
+
// SQLite has no named types, so a domain reaches the database only as a
|
|
335
|
+
// constraint on the column that uses it. The values sort as text rather than
|
|
336
|
+
// in declaration order — the one thing the rendering cannot reproduce, and
|
|
337
|
+
// why the enum's `values:` order is documented as PostgreSQL's alone.
|
|
338
|
+
if (column.enum) {
|
|
339
|
+
const values = column.enum.values.map((value) => literal(value)).join(", ");
|
|
340
|
+
parts.push(`CHECK (${this.quote(column.name)} IN (${values}))`);
|
|
341
|
+
}
|
|
291
342
|
const def = columnDefault(column);
|
|
292
343
|
return parts.join(" ") + def;
|
|
293
344
|
}
|
|
@@ -303,10 +354,14 @@ export class SqliteSchemaDriver implements SchemaDriver {
|
|
|
303
354
|
createTable(schema: string, table: DeclaredTable): string[] {
|
|
304
355
|
const parts = table.columns.map((column) => this.#columnDefinition(column));
|
|
305
356
|
// Foreign keys are part of the table in SQLite — there is no ADD CONSTRAINT
|
|
306
|
-
// — so they are emitted here and nowhere else.
|
|
357
|
+
// — so they are emitted here and nowhere else. Named checks land in exactly
|
|
358
|
+
// the same place, for exactly the same reason.
|
|
307
359
|
for (const fk of table.foreignKeys) {
|
|
308
360
|
parts.push(this.#foreignKeyClause(fk));
|
|
309
361
|
}
|
|
362
|
+
for (const check of table.checks) {
|
|
363
|
+
parts.push(`CONSTRAINT ${this.quote(check.name)} CHECK (${check.expression})`);
|
|
364
|
+
}
|
|
310
365
|
return [
|
|
311
366
|
`CREATE TABLE IF NOT EXISTS ${this.qualify(schema, table.name)} (\n ${parts.join(",\n ")}\n)`,
|
|
312
367
|
];
|
|
@@ -377,14 +432,150 @@ export class SqliteSchemaDriver implements SchemaDriver {
|
|
|
377
432
|
);
|
|
378
433
|
}
|
|
379
434
|
|
|
435
|
+
/** SQLite has no `ADD CONSTRAINT`, so a check exists only as part of the table
|
|
436
|
+
* it was created with — the same place its foreign keys are. */
|
|
437
|
+
readonly checksInCreateTable = true;
|
|
438
|
+
|
|
439
|
+
/** Compared against the RECORDED declaration rather than live state, so this
|
|
440
|
+
* is declaration against declaration and the text is the whole comparison. */
|
|
441
|
+
checkDiffers(live: LiveCheck, declared: DeclaredCheck): boolean {
|
|
442
|
+
return live.expression !== declared.expression;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
classifyCheckChange(_live: LiveCheck, declared: DeclaredCheck): ChangeSafety {
|
|
446
|
+
return {
|
|
447
|
+
safe: false,
|
|
448
|
+
reason:
|
|
449
|
+
`SQLite has no ALTER for a constraint — a check exists only as part of the table it was ` +
|
|
450
|
+
`created with — so '${declared.name}' cannot be changed in place. Rebuild the table in a ` +
|
|
451
|
+
`'migrations:' entry.`,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
addCheck(_schema: string, table: string, check: DeclaredCheck): string[] {
|
|
456
|
+
throw new Error(
|
|
457
|
+
`SQLite.Table: check '${check.name}' cannot be added to the existing table '${table}' — ` +
|
|
458
|
+
`SQLite has no ADD CONSTRAINT and a check exists only as part of the table it was ` +
|
|
459
|
+
`created with. Rebuild the table in a 'migrations:' entry.`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
dropCheck(_schema: string, table: string, name: string): string[] {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`SQLite.Table: check '${name}' cannot be dropped from '${table}' — SQLite has no DROP ` +
|
|
466
|
+
`CONSTRAINT. Rebuild the table in a 'migrations:' entry.`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Unreachable: nothing on SQLite is ever added `NOT VALID`, so nothing is
|
|
471
|
+
* waiting to be proven. */
|
|
472
|
+
validateCheck(_schema: string, table: string, name: string): string[] {
|
|
473
|
+
throw new Error(`SQLite.Table: check '${table}.${name}' has no deferred validation to run.`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* SQLite has no named types, so a domain has no database object behind it: the
|
|
478
|
+
* values are rendered as a `CHECK` on every column that references the enum.
|
|
479
|
+
*
|
|
480
|
+
* The DECLARATION is still a ledger object like any other, which is what lets a
|
|
481
|
+
* change to it be detected at all — see `classifyEnumChange`.
|
|
482
|
+
*/
|
|
483
|
+
readonly namedEnumTypes = false;
|
|
484
|
+
|
|
485
|
+
/** Nothing to read back: there is no type to introspect. */
|
|
486
|
+
async introspectEnums(): Promise<LiveEnum[]> {
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Unreachable: the shared half asks only when `namedEnumTypes` holds. */
|
|
491
|
+
createEnum(_schema: string, declared: DeclaredEnum): string[] {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`SQLite.Enum '${declared.typeName}': SQLite has no named types, so nothing creates one.`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
addEnumValues(_schema: string, declared: DeclaredEnum): string[] {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`SQLite.Enum '${declared.typeName}': SQLite has no named types, so nothing alters one.`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** No statement at all — the `CHECK`s on referencing tables never named the
|
|
504
|
+
* type, so the ledger key rewrite IS the rename. */
|
|
505
|
+
renameEnum(): string[] {
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** The one part of renaming where the two engines need no separate story:
|
|
510
|
+
* SQLite renames a table with the same statement PostgreSQL does. */
|
|
511
|
+
renameTable(schema: string, from: string, to: string): string[] {
|
|
512
|
+
return [`ALTER TABLE ${this.qualify(schema, from)} RENAME TO ${this.quote(to)}`];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* A domain reaches the database inside the tables that use it, so changing one
|
|
517
|
+
* would mean rebuilding every referencing table — which is where SQLite's
|
|
518
|
+
* foreign keys and column alterations already are.
|
|
519
|
+
*
|
|
520
|
+
* The comparison is against the RECORDED declaration rather than live state,
|
|
521
|
+
* because there is no live state: nothing in the database corresponds to the
|
|
522
|
+
* type. That is exactly what makes snapshotting the whole enum declaration
|
|
523
|
+
* load-bearing rather than decorative.
|
|
524
|
+
*/
|
|
525
|
+
classifyEnumChange(
|
|
526
|
+
_live: LiveEnum | undefined,
|
|
527
|
+
declared: DeclaredEnum,
|
|
528
|
+
owned: DeclaredEnum | undefined,
|
|
529
|
+
): ChangeSafety {
|
|
530
|
+
if (!owned) return { safe: true };
|
|
531
|
+
const same =
|
|
532
|
+
owned.baseType === declared.baseType &&
|
|
533
|
+
owned.values.length === declared.values.length &&
|
|
534
|
+
owned.values.every((value, i) => value === declared.values[i]);
|
|
535
|
+
if (same) return { safe: true };
|
|
536
|
+
return {
|
|
537
|
+
safe: false,
|
|
538
|
+
reason:
|
|
539
|
+
`SQLite has no named types, so '${declared.typeName}' is rendered as a CHECK on every ` +
|
|
540
|
+
`column that references it — and SQLite has no ALTER for a constraint. Changing the ` +
|
|
541
|
+
`declaration would leave every existing table enforcing the old values. Rebuild the ` +
|
|
542
|
+
`referencing tables in a 'migrations:' entry, which is where a changed constraint on ` +
|
|
543
|
+
`this engine belongs.`,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
dropEnum(): string[] {
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** SQLite has no installable extensions in the sense this declares — a
|
|
552
|
+
* loadable extension is a build-time or connection-time concern, not a schema
|
|
553
|
+
* object a pass can create. */
|
|
554
|
+
readonly namedExtensions = false;
|
|
555
|
+
|
|
556
|
+
async introspectExtensions(): Promise<string[]> {
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
createExtension(_schema: string, name: string): string[] {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`SQLite.Schema: extension '${name}' cannot be created — SQLite has no installable ` +
|
|
563
|
+
`extensions a schema pass can provision.`,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
dropExtension(_schema: string, name: string): string[] {
|
|
568
|
+
throw new Error(`SQLite.Schema: extension '${name}' cannot be dropped.`);
|
|
569
|
+
}
|
|
570
|
+
|
|
380
571
|
canReclaim(id: SchemaObjectId): ChangeSafety {
|
|
381
|
-
if (id.kind === "foreignKey") {
|
|
572
|
+
if (id.kind === "foreignKey" || id.kind === "check") {
|
|
382
573
|
return {
|
|
383
574
|
safe: false,
|
|
384
575
|
reason:
|
|
385
|
-
|
|
386
|
-
"Rebuild the table in a 'migrations:' entry; the tombstone
|
|
387
|
-
"constraint is gone.",
|
|
576
|
+
`SQLite has no DROP CONSTRAINT, so this ${id.kind === "check" ? "check" : "foreign key"} ` +
|
|
577
|
+
"cannot be dropped in place. Rebuild the table in a 'migrations:' entry; the tombstone " +
|
|
578
|
+
"is cleared when the constraint is gone.",
|
|
388
579
|
};
|
|
389
580
|
}
|
|
390
581
|
return { safe: true };
|
|
@@ -397,4 +588,34 @@ export class SqliteSchemaDriver implements SchemaDriver {
|
|
|
397
588
|
dropTable(schema: string, table: string): string[] {
|
|
398
589
|
return [`DROP TABLE IF EXISTS ${this.qualify(schema, table)}`];
|
|
399
590
|
}
|
|
591
|
+
|
|
592
|
+
/** SQLite has had upsert since 3.24, so both engines render the same statement
|
|
593
|
+
* SHAPE — which is why it lives in `row-statements.ts` and only the dialect
|
|
594
|
+
* is answered here. */
|
|
595
|
+
get #rowDialect(): RowDialect {
|
|
596
|
+
return {
|
|
597
|
+
quote: (name) => this.quote(name),
|
|
598
|
+
literal,
|
|
599
|
+
qualify: (schema, table) => this.qualify(schema, table),
|
|
600
|
+
excludedAlias: "excluded",
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
upsertRow(
|
|
605
|
+
schema: string,
|
|
606
|
+
table: string,
|
|
607
|
+
key: readonly string[],
|
|
608
|
+
row: Record<string, unknown>,
|
|
609
|
+
): string[] {
|
|
610
|
+
return upsertRowStatements(this.#rowDialect, schema, table, key, row);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
deleteRow(
|
|
614
|
+
schema: string,
|
|
615
|
+
table: string,
|
|
616
|
+
key: readonly string[],
|
|
617
|
+
row: Record<string, unknown>,
|
|
618
|
+
): string[] {
|
|
619
|
+
return deleteRowStatements(this.#rowDialect, schema, table, key, row);
|
|
620
|
+
}
|
|
400
621
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import {
|
|
3
|
+
enumReferenceResolver,
|
|
3
4
|
normalizeTable,
|
|
4
5
|
tableReferenceResolver,
|
|
6
|
+
type ColumnTypeResolver,
|
|
5
7
|
type DeclaredTable,
|
|
6
8
|
type RawTable,
|
|
7
9
|
} from "@telorun/sql";
|
|
@@ -17,7 +19,11 @@ export class SqliteTableResource implements ResourceInstance {
|
|
|
17
19
|
readonly declaration: DeclaredTable;
|
|
18
20
|
|
|
19
21
|
constructor(raw: RawTable, ctx: ResourceContext) {
|
|
20
|
-
this.declaration = normalizeTable(
|
|
22
|
+
this.declaration = normalizeTable(
|
|
23
|
+
raw,
|
|
24
|
+
tableReferenceResolver(ctx, "SQLite.Table", raw.table),
|
|
25
|
+
sqliteColumnType(ctx, raw.table),
|
|
26
|
+
);
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
/** The physical table name, read by consumers that build statements against
|
|
@@ -31,6 +37,27 @@ export class SqliteTableResource implements ResourceInstance {
|
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
/** SQLite has no named types, so an enum column's engine-native type is the
|
|
41
|
+
* enum's declared base storage class; the values ride along as a CHECK. */
|
|
42
|
+
function sqliteColumnType(ctx: ResourceContext, table: string): ColumnTypeResolver {
|
|
43
|
+
const resolveEnum = enumReferenceResolver(ctx, "SQLite.Enum", table);
|
|
44
|
+
return (value, column) => {
|
|
45
|
+
const declared = resolveEnum(value, column);
|
|
46
|
+
if (!declared) return { type: String(value) };
|
|
47
|
+
if (!declared.baseType) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`SQLite.Table '${table}': column '${column}' references enum '${declared.typeName}', ` +
|
|
50
|
+
`which declares no 'baseType'. SQLite has no named types, so something has to say ` +
|
|
51
|
+
`which storage class the values sit in.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
type: declared.baseType,
|
|
56
|
+
enum: { typeName: declared.typeName, values: declared.values },
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
export function register(): void {}
|
|
35
62
|
|
|
36
63
|
export async function create(
|