@metaobjectsdev/migrate-ts 0.15.20 → 0.15.21
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/README.md +2 -1
- package/dist/diff/index.js +73 -15
- package/dist/diff/index.js.map +1 -1
- package/dist/diff/status.d.ts.map +1 -1
- package/dist/diff/status.js +30 -3
- package/dist/diff/status.js.map +1 -1
- package/dist/emit/sqlite.d.ts.map +1 -1
- package/dist/emit/sqlite.js +64 -16
- package/dist/emit/sqlite.js.map +1 -1
- package/dist/expected-schema.d.ts.map +1 -1
- package/dist/expected-schema.js +98 -6
- package/dist/expected-schema.js.map +1 -1
- package/dist/introspect/d1.d.ts.map +1 -1
- package/dist/introspect/d1.js +47 -12
- package/dist/introspect/d1.js.map +1 -1
- package/dist/introspect/postgres.d.ts.map +1 -1
- package/dist/introspect/postgres.js +35 -13
- package/dist/introspect/postgres.js.map +1 -1
- package/dist/introspect/sqlite-shared.d.ts +60 -1
- package/dist/introspect/sqlite-shared.d.ts.map +1 -1
- package/dist/introspect/sqlite-shared.js +205 -3
- package/dist/introspect/sqlite-shared.js.map +1 -1
- package/dist/introspect/sqlite.d.ts.map +1 -1
- package/dist/introspect/sqlite.js +27 -13
- package/dist/introspect/sqlite.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/diff/index.ts +83 -14
- package/src/diff/status.ts +34 -3
- package/src/emit/sqlite.ts +63 -16
- package/src/expected-schema.ts +98 -7
- package/src/introspect/d1.ts +57 -11
- package/src/introspect/postgres.ts +35 -13
- package/src/introspect/sqlite-shared.ts +197 -4
- package/src/introspect/sqlite.ts +37 -12
- package/src/types.ts +7 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* All three routines are pure mappings of SQLite's declared type / pragma
|
|
6
6
|
* values to canonical migrate-ts types and carry no I/O dependencies.
|
|
7
7
|
*/
|
|
8
|
-
import type { ColumnDefault, FkAction } from "../types.js";
|
|
8
|
+
import type { CheckDescriptor, ColumnDefault, FkAction, IndexDescriptor } from "../types.js";
|
|
9
9
|
import type { SqlType } from "../sql-type.js";
|
|
10
10
|
|
|
11
11
|
export const SQLITE_EXPR_DEFAULT_PATTERNS = [
|
|
@@ -17,11 +17,27 @@ export const SQLITE_EXPR_DEFAULT_PATTERNS = [
|
|
|
17
17
|
|
|
18
18
|
export function parseSqliteDefault(raw: string | null): ColumnDefault | undefined {
|
|
19
19
|
if (raw === null || raw === undefined || raw === "") return undefined;
|
|
20
|
+
|
|
21
|
+
// A QUOTE-WRAPPED value is a string literal BY CONSTRUCTION — SQLite always quotes a
|
|
22
|
+
// literal string default. This test MUST come before the expr patterns: one of those
|
|
23
|
+
// patterns is a bare /\(.*\)/, so a perfectly ordinary literal containing parentheses
|
|
24
|
+
// (`@default "n/a (unknown)"` → stored as `'n/a (unknown)'`) would otherwise be
|
|
25
|
+
// classified an EXPR with its quotes still attached. It could then never string-equal
|
|
26
|
+
// the expected literal, so the diff would report change-column-default on EVERY run —
|
|
27
|
+
// and on SQLite (no ALTER COLUMN) that recreate-and-copies the whole table, forever,
|
|
28
|
+
// un-gated, with `verify --db` permanently red. Exactly the failure this un-escaping
|
|
29
|
+
// was added to prevent.
|
|
30
|
+
//
|
|
31
|
+
// Then un-double the emitter's `''` escaping (`@default "don't"` → `'don''t'`), or the
|
|
32
|
+
// introspected `don''t` never equals the expected `don't` — same perpetual rebuild.
|
|
33
|
+
const quoted = /^'([\s\S]*)'$/.exec(raw);
|
|
34
|
+
if (quoted !== null) {
|
|
35
|
+
return { kind: "literal", value: quoted[1]!.replace(/''/g, "'") };
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
const isExpr = SQLITE_EXPR_DEFAULT_PATTERNS.some((re) => re.test(raw));
|
|
21
39
|
if (isExpr) return { kind: "expr", value: raw };
|
|
22
|
-
|
|
23
|
-
const cleaned = raw.replace(/^'(.*)'$/, "$1");
|
|
24
|
-
return { kind: "literal", value: cleaned };
|
|
40
|
+
return { kind: "literal", value: raw };
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
export function sqliteTypeToSqlType(declaredType: string): SqlType {
|
|
@@ -68,6 +84,183 @@ export function sqliteTypeToSqlType(declaredType: string): SqlType {
|
|
|
68
84
|
return { kind: "text" };
|
|
69
85
|
}
|
|
70
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Parse NAMED CHECK constraints (`CONSTRAINT <name> CHECK (<expr>)`) out of a
|
|
89
|
+
* table's CREATE TABLE statement (sqlite_master.sql). SQLite exposes no pragma
|
|
90
|
+
* for CHECK constraints, so the stored DDL text is the only catalog.
|
|
91
|
+
*
|
|
92
|
+
* This is what makes CHECK evolution CONVERGE on sqlite: the diff proposes a
|
|
93
|
+
* check change → the emitter recreate-and-copies the table with the new inline
|
|
94
|
+
* CHECK → this reads the very DDL that recreate wrote, so the re-diff is empty.
|
|
95
|
+
* Without it the actual side always reported `checks: []` and every expected
|
|
96
|
+
* check re-surfaced as add-check on every single run.
|
|
97
|
+
*
|
|
98
|
+
* Unnamed checks (`CHECK (…)` with no CONSTRAINT clause — hand-written DDL) are
|
|
99
|
+
* NOT parsed: they have no identity to match on. A modeled check over such a
|
|
100
|
+
* table converges after one recreate (which rewrites it in named form).
|
|
101
|
+
*
|
|
102
|
+
* The expression is scanned with balanced parens and string-literal awareness
|
|
103
|
+
* (an enum member may contain `(`/`)`), and returned verbatim — the diff's
|
|
104
|
+
* checkExprEquals normalizes both sides before comparing.
|
|
105
|
+
*/
|
|
106
|
+
export function parseSqliteChecks(createSql: string | null | undefined): CheckDescriptor[] {
|
|
107
|
+
if (createSql === null || createSql === undefined || createSql === "") return [];
|
|
108
|
+
const out: CheckDescriptor[] = [];
|
|
109
|
+
const re = /\bCONSTRAINT\s+(?:"((?:[^"]|"")+)"|([A-Za-z_][A-Za-z0-9_$]*))\s+CHECK\s*\(/gi;
|
|
110
|
+
let m: RegExpExecArray | null;
|
|
111
|
+
while ((m = re.exec(createSql)) !== null) {
|
|
112
|
+
const name = m[1] !== undefined ? m[1].replace(/""/g, '"') : m[2]!;
|
|
113
|
+
const open = re.lastIndex - 1; // position of the "(" the regex just consumed
|
|
114
|
+
let depth = 0;
|
|
115
|
+
let inString = false;
|
|
116
|
+
let close = -1;
|
|
117
|
+
for (let i = open; i < createSql.length; i++) {
|
|
118
|
+
const ch = createSql[i];
|
|
119
|
+
if (inString) {
|
|
120
|
+
if (ch === "'") {
|
|
121
|
+
if (createSql[i + 1] === "'") i++; // '' escape inside the literal
|
|
122
|
+
else inString = false;
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (ch === "'") inString = true;
|
|
127
|
+
else if (ch === "(") depth++;
|
|
128
|
+
else if (ch === ")") {
|
|
129
|
+
depth--;
|
|
130
|
+
if (depth === 0) { close = i; break; }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (close === -1) break; // malformed tail — stop rather than mis-slice
|
|
134
|
+
out.push({ name, expression: createSql.slice(open + 1, close).trim() });
|
|
135
|
+
re.lastIndex = close + 1;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Skip a single-quoted SQL string starting at `i` (position of the opening
|
|
141
|
+
* quote); returns the position just past the closing quote ('' escapes honored). */
|
|
142
|
+
function skipSingleQuoted(s: string, i: number): number {
|
|
143
|
+
i++;
|
|
144
|
+
while (i < s.length) {
|
|
145
|
+
if (s[i] === "'") {
|
|
146
|
+
if (s[i + 1] === "'") { i += 2; continue; }
|
|
147
|
+
return i + 1;
|
|
148
|
+
}
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
return i;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Skip a double-quoted SQL identifier starting at `i`; "" escapes honored. */
|
|
155
|
+
function skipDoubleQuoted(s: string, i: number): number {
|
|
156
|
+
i++;
|
|
157
|
+
while (i < s.length) {
|
|
158
|
+
if (s[i] === '"') {
|
|
159
|
+
if (s[i + 1] === '"') { i += 2; continue; }
|
|
160
|
+
return i + 1;
|
|
161
|
+
}
|
|
162
|
+
i++;
|
|
163
|
+
}
|
|
164
|
+
return i;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface ParsedSqliteIndexDef {
|
|
168
|
+
/** Raw key-list text between the balanced parens after `ON <table>`. */
|
|
169
|
+
keyList: string;
|
|
170
|
+
/** Raw partial-index predicate after WHERE; undefined for a full index. */
|
|
171
|
+
where?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Parse a stored `CREATE [UNIQUE] INDEX … ON <table> (<keys>) [WHERE <pred>]`
|
|
176
|
+
* statement (sqlite_master.sql). SQLite has no pragma exposing an index's key
|
|
177
|
+
* EXPRESSIONS or its partial-index predicate — the stored DDL is the only
|
|
178
|
+
* catalog — so this powers reading `@expr` / `@where` indexes back for the diff
|
|
179
|
+
* to converge. Quote-aware: parens inside string literals or quoted identifiers
|
|
180
|
+
* never confuse the balanced scan.
|
|
181
|
+
*/
|
|
182
|
+
export function parseSqliteIndexDef(createSql: string | null | undefined): ParsedSqliteIndexDef | undefined {
|
|
183
|
+
if (createSql === null || createSql === undefined || createSql === "") return undefined;
|
|
184
|
+
const n = createSql.length;
|
|
185
|
+
// First "(" outside any quoted region = start of the key list.
|
|
186
|
+
let open = -1;
|
|
187
|
+
for (let i = 0; i < n; ) {
|
|
188
|
+
const ch = createSql[i];
|
|
189
|
+
if (ch === "'") { i = skipSingleQuoted(createSql, i); continue; }
|
|
190
|
+
if (ch === '"') { i = skipDoubleQuoted(createSql, i); continue; }
|
|
191
|
+
if (ch === "(") { open = i; break; }
|
|
192
|
+
i++;
|
|
193
|
+
}
|
|
194
|
+
if (open === -1) return undefined;
|
|
195
|
+
// Balanced scan (quote-aware) to the matching ")".
|
|
196
|
+
let close = -1;
|
|
197
|
+
for (let i = open, depth = 0; i < n; ) {
|
|
198
|
+
const ch = createSql[i];
|
|
199
|
+
if (ch === "'") { i = skipSingleQuoted(createSql, i); continue; }
|
|
200
|
+
if (ch === '"') { i = skipDoubleQuoted(createSql, i); continue; }
|
|
201
|
+
if (ch === "(") depth++;
|
|
202
|
+
else if (ch === ")") {
|
|
203
|
+
depth--;
|
|
204
|
+
if (depth === 0) { close = i; break; }
|
|
205
|
+
}
|
|
206
|
+
i++;
|
|
207
|
+
}
|
|
208
|
+
if (close === -1) return undefined;
|
|
209
|
+
const out: ParsedSqliteIndexDef = { keyList: createSql.slice(open + 1, close).trim() };
|
|
210
|
+
const m = /^\s*WHERE\s+([\s\S]+)$/i.exec(createSql.slice(close + 1));
|
|
211
|
+
if (m) out.where = m[1]!.trim().replace(/;\s*$/, "");
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** pragma_index_list row, dialect-neutrally coerced by the caller. */
|
|
216
|
+
export interface SqliteIndexListEntry {
|
|
217
|
+
name: string;
|
|
218
|
+
unique: boolean;
|
|
219
|
+
partial: boolean;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A KEY column row from pragma_index_xinfo (key=1 rows only, seqno order). */
|
|
223
|
+
export interface SqliteIndexKeyColumn {
|
|
224
|
+
/** Column name; null for an expression key (cid = -2). */
|
|
225
|
+
name: string | null;
|
|
226
|
+
/** true when the key is sorted DESC. */
|
|
227
|
+
desc: boolean;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Assemble an IndexDescriptor from the SQLite catalog pieces: pragma_index_list
|
|
232
|
+
* (unique/partial), pragma_index_xinfo key columns (names + DESC bits; name null
|
|
233
|
+
* for an expression key), and the stored CREATE INDEX DDL (expression key list +
|
|
234
|
+
* partial predicate — neither is exposed by any pragma).
|
|
235
|
+
*
|
|
236
|
+
* Mirrors the expected side's shape rules: an expression index carries the whole
|
|
237
|
+
* key list in `expr` with `columns: []`; `orders` is attached only when some key
|
|
238
|
+
* is DESC (all-ascending serializes as absent, like buildExpectedSchema).
|
|
239
|
+
*/
|
|
240
|
+
export function buildSqliteIndexDescriptor(
|
|
241
|
+
entry: SqliteIndexListEntry,
|
|
242
|
+
keyColumns: readonly SqliteIndexKeyColumn[],
|
|
243
|
+
createSql: string | null | undefined,
|
|
244
|
+
): IndexDescriptor {
|
|
245
|
+
const parsed = parseSqliteIndexDef(createSql);
|
|
246
|
+
const descriptor: IndexDescriptor = {
|
|
247
|
+
name: entry.name,
|
|
248
|
+
columns: [],
|
|
249
|
+
unique: entry.unique,
|
|
250
|
+
};
|
|
251
|
+
if (keyColumns.some((c) => c.name === null)) {
|
|
252
|
+
// Expression key somewhere in the list → the whole key list is the expr
|
|
253
|
+
// (same convention as the Postgres introspector + buildExpectedSchema).
|
|
254
|
+
if (parsed !== undefined) descriptor.expr = parsed.keyList;
|
|
255
|
+
} else {
|
|
256
|
+
descriptor.columns = keyColumns.map((c) => c.name!);
|
|
257
|
+
const orders = keyColumns.map((c): "asc" | "desc" => (c.desc ? "desc" : "asc"));
|
|
258
|
+
if (orders.some((o) => o === "desc")) descriptor.orders = orders;
|
|
259
|
+
}
|
|
260
|
+
if (entry.partial && parsed?.where !== undefined) descriptor.where = parsed.where;
|
|
261
|
+
return descriptor;
|
|
262
|
+
}
|
|
263
|
+
|
|
71
264
|
export function sqliteRuleToAction(rule: string): FkAction {
|
|
72
265
|
const r = rule.toUpperCase();
|
|
73
266
|
if (r === "CASCADE") return "cascade";
|
package/src/introspect/sqlite.ts
CHANGED
|
@@ -4,7 +4,10 @@ import type {
|
|
|
4
4
|
SchemaSnapshot, TableDescriptor, ColumnDescriptor, SnapshotMeta,
|
|
5
5
|
IndexDescriptor, FkDescriptor, FkAction, ViewDescriptor,
|
|
6
6
|
} from "../types.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
parseSqliteDefault, sqliteTypeToSqlType, sqliteRuleToAction, parseSqliteChecks,
|
|
9
|
+
buildSqliteIndexDescriptor,
|
|
10
|
+
} from "./sqlite-shared.js";
|
|
8
11
|
import { MIGRATIONS_TABLE } from "../apply/ledger.js";
|
|
9
12
|
|
|
10
13
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -16,9 +19,14 @@ export async function introspectSqlite(db: Kysely<Record<string, unknown>>): Pro
|
|
|
16
19
|
const versionRow = await sql<{ v: string }>`SELECT sqlite_version() AS v`.execute(k);
|
|
17
20
|
const meta: SnapshotMeta = { sqliteVersion: versionRow.rows[0]?.v ?? "0.0.0" };
|
|
18
21
|
|
|
22
|
+
// NOTE: "_" is a single-character WILDCARD in SQL LIKE. Unescaped, '__new_%' also
|
|
23
|
+
// matches an ordinary table named "renewals" (verified against real SQLite), silently
|
|
24
|
+
// hiding it from introspection — so the diff re-proposes CREATE TABLE on every run and
|
|
25
|
+
// the next apply dies with "already exists". Escape the underscores.
|
|
19
26
|
const tableNamesRows = await sql<{ name: string; sql: string | null }>`
|
|
20
27
|
SELECT name, sql FROM sqlite_master
|
|
21
|
-
WHERE type='table' AND name NOT LIKE '
|
|
28
|
+
WHERE type='table' AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'
|
|
29
|
+
AND name NOT LIKE '\\_\\_new\\_%' ESCAPE '\\'
|
|
22
30
|
AND name <> ${MIGRATIONS_TABLE}
|
|
23
31
|
ORDER BY name
|
|
24
32
|
`.execute(k);
|
|
@@ -38,7 +46,9 @@ export async function introspectSqlite(db: Kysely<Record<string, unknown>>): Pro
|
|
|
38
46
|
columns: cols,
|
|
39
47
|
indexes: await readSqliteIndexes(k, t.name),
|
|
40
48
|
foreignKeys: await readSqliteForeignKeys(k, t.name),
|
|
41
|
-
|
|
49
|
+
// Named CHECKs parsed from the stored CREATE TABLE DDL — required for
|
|
50
|
+
// check evolution (enum @values changes) to converge on sqlite.
|
|
51
|
+
checks: parseSqliteChecks(t.sql),
|
|
42
52
|
primaryKey: pk,
|
|
43
53
|
});
|
|
44
54
|
}
|
|
@@ -52,7 +62,7 @@ async function readSqliteViews(k: RawKysely): Promise<ViewDescriptor[]> {
|
|
|
52
62
|
// We carry it through on the descriptor so the diff can detect view-body
|
|
53
63
|
// drift (not just name presence).
|
|
54
64
|
const rows = await sql<{ name: string; sql: string | null }>`
|
|
55
|
-
SELECT name, sql FROM sqlite_master WHERE type='view' AND name NOT LIKE '
|
|
65
|
+
SELECT name, sql FROM sqlite_master WHERE type='view' AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'
|
|
56
66
|
ORDER BY name
|
|
57
67
|
`.execute(k);
|
|
58
68
|
return rows.rows.map((r) => {
|
|
@@ -99,18 +109,33 @@ async function readSqliteIndexes(k: RawKysely, table: string): Promise<IndexDesc
|
|
|
99
109
|
SELECT * FROM pragma_index_list(${table})
|
|
100
110
|
`.execute(k);
|
|
101
111
|
|
|
112
|
+
// Stored CREATE INDEX DDL per index — the ONLY catalog for an index's key
|
|
113
|
+
// EXPRESSIONS and partial-index WHERE predicate (no pragma exposes either).
|
|
114
|
+
// Auto-created indexes (column UNIQUE constraints, origin 'u') have sql NULL,
|
|
115
|
+
// which is fine: they can't be partial or expression-keyed.
|
|
116
|
+
const sqlRows = await sql<{ name: string; sql: string | null }>`
|
|
117
|
+
SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name = ${table}
|
|
118
|
+
`.execute(k);
|
|
119
|
+
const ddlByName = new Map(sqlRows.rows.map((r) => [r.name, r.sql] as const));
|
|
120
|
+
|
|
102
121
|
const indexes: IndexDescriptor[] = [];
|
|
103
122
|
for (const ix of listRows.rows) {
|
|
104
123
|
if (ix.origin === "pk") continue; // PK index — excluded (lives in TableDescriptor.primaryKey)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
// pragma_index_xinfo (not index_info): includes the DESC bit and marks key
|
|
125
|
+
// columns (key=1) vs auxiliary rowid columns; an expression key has name
|
|
126
|
+
// NULL. SELECT * avoids "desc"/"key" reserved-keyword issues in libsql.
|
|
127
|
+
const xinfo = await sql<{
|
|
128
|
+
seqno: number; cid: number; name: string | null; desc: number; coll: string; key: number;
|
|
129
|
+
}>`
|
|
130
|
+
SELECT * FROM pragma_index_xinfo(${ix.name}) ORDER BY seqno
|
|
108
131
|
`.execute(k);
|
|
109
|
-
indexes.push(
|
|
110
|
-
name: ix.name,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
indexes.push(buildSqliteIndexDescriptor(
|
|
133
|
+
{ name: ix.name, unique: ix.unique === 1, partial: ix.partial === 1 },
|
|
134
|
+
xinfo.rows
|
|
135
|
+
.filter((c) => c.key === 1)
|
|
136
|
+
.map((c) => ({ name: c.name, desc: c.desc === 1 })),
|
|
137
|
+
ddlByName.get(ix.name) ?? null,
|
|
138
|
+
));
|
|
114
139
|
}
|
|
115
140
|
return indexes;
|
|
116
141
|
}
|
package/src/types.ts
CHANGED
|
@@ -197,6 +197,13 @@ export interface AllowOptions {
|
|
|
197
197
|
dropIndex?: boolean;
|
|
198
198
|
dropFk?: boolean;
|
|
199
199
|
dropCheck?: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Gates a REAL view removal (present in the DB, absent from the model) — like
|
|
202
|
+
* every other drop. The internal drop/create recreate pair the diff emits
|
|
203
|
+
* around a column-altering change to a view's source table is NOT gated (the
|
|
204
|
+
* view is re-created in the same migration).
|
|
205
|
+
*/
|
|
206
|
+
dropView?: boolean;
|
|
200
207
|
/** Existing data must satisfy NOT NULL; diff cannot verify this. */
|
|
201
208
|
nullableToNotNull?: boolean;
|
|
202
209
|
}
|