@metaobjectsdev/migrate-ts 0.10.0 → 0.11.0-rc.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/dist/diff/index.d.ts +16 -0
- package/dist/diff/index.d.ts.map +1 -1
- package/dist/diff/index.js +44 -5
- package/dist/diff/index.js.map +1 -1
- package/dist/emit/postgres.js +13 -1
- package/dist/emit/postgres.js.map +1 -1
- package/dist/expected-schema.d.ts.map +1 -1
- package/dist/expected-schema.js +136 -7
- package/dist/expected-schema.js.map +1 -1
- package/dist/introspect/postgres.d.ts +10 -3
- package/dist/introspect/postgres.d.ts.map +1 -1
- package/dist/introspect/postgres.js +89 -12
- package/dist/introspect/postgres.js.map +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/diff/index.ts +63 -5
- package/src/emit/postgres.ts +13 -1
- package/src/expected-schema.ts +148 -6
- package/src/introspect/postgres.ts +113 -16
- package/src/types.ts +25 -0
|
@@ -151,25 +151,36 @@ export function pgTypeToSqlType(dataType: string, maxLength?: number | null): Sq
|
|
|
151
151
|
* Returns undefined if the default is absent or empty.
|
|
152
152
|
*
|
|
153
153
|
* Classification rules:
|
|
154
|
-
* - Expressions: now(), CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME,
|
|
155
|
-
* nextval(...),
|
|
156
|
-
*
|
|
154
|
+
* - Expressions: now(), CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME, and any
|
|
155
|
+
* bare function-call (e.g. nextval(...), gen_random_uuid(), uuid_generate_v4()),
|
|
156
|
+
* plus any value that starts with a non-quote character and contains a `::` cast
|
|
157
|
+
* (i.e. bare identifier with cast, like `NULL::text`).
|
|
157
158
|
* - Literals: `'value'` (optionally followed by `::type` cast, which PG
|
|
158
159
|
* commonly appends for clarity). The cast is stripped; the value is unquoted.
|
|
159
160
|
*
|
|
160
161
|
* PG stores literal booleans as `'true'::boolean`, integers as `'42'::integer`,
|
|
161
162
|
* strings as `'hello'::text` — all are literals after stripping the cast.
|
|
163
|
+
*
|
|
164
|
+
* The bare function-call rule keeps this in lockstep with the metadata-side
|
|
165
|
+
* default classifier (expected-schema's EXPR_DEFAULT_PATTERNS, which treats any
|
|
166
|
+
* `()` default as an expression): without it, a `gen_random_uuid()` column default
|
|
167
|
+
* round-trips as a literal here but an expression there, producing a spurious
|
|
168
|
+
* column diff on every uuid-PK table.
|
|
162
169
|
*/
|
|
163
170
|
export function parsePgDefault(raw: string | null | undefined): ColumnDefault | undefined {
|
|
164
171
|
if (raw === undefined || raw === null || raw === "") return undefined;
|
|
165
172
|
|
|
166
|
-
// Function-call or keyword expressions
|
|
173
|
+
// Function-call or keyword expressions. The leading-identifier-then-"(" rule
|
|
174
|
+
// matches any bare function call (gen_random_uuid(), uuid_generate_v4(), …)
|
|
175
|
+
// while never matching a quoted literal (those start with a single quote and are
|
|
176
|
+
// handled below).
|
|
167
177
|
if (
|
|
168
178
|
/^now\(\)$/i.test(raw) ||
|
|
169
179
|
/^current_timestamp\b/i.test(raw) ||
|
|
170
180
|
/^current_date\b/i.test(raw) ||
|
|
171
181
|
/^current_time\b/i.test(raw) ||
|
|
172
|
-
/^nextval\(/i.test(raw)
|
|
182
|
+
/^nextval\(/i.test(raw) ||
|
|
183
|
+
/^[a-zA-Z_][\w.]*\s*\(/.test(raw)
|
|
173
184
|
) {
|
|
174
185
|
return { kind: "expr", value: raw };
|
|
175
186
|
}
|
|
@@ -317,46 +328,132 @@ async function readPgIndexes(k: RawKysely, schema: string, table: string): Promi
|
|
|
317
328
|
// pg-mem gap: array_position() is not implemented, so this query throws on
|
|
318
329
|
// pg-mem. We catch and return [] so non-index tests still pass against pg-mem.
|
|
319
330
|
// Real PG (Postgres 16) handles this correctly.
|
|
320
|
-
|
|
331
|
+
// One row per index KEY, in key order, carrying: the column name (NULL for an
|
|
332
|
+
// expression key — attnum 0), the DESC bit from `indoption`, and the partial-index
|
|
333
|
+
// predicate (`indpred`, constant per index). unnest WITH ORDINALITY over indkey +
|
|
334
|
+
// indoption keeps the key order and pairs each key with its option flags.
|
|
335
|
+
let rows: {
|
|
336
|
+
rows: Array<{
|
|
337
|
+
index_name: string;
|
|
338
|
+
is_unique: boolean;
|
|
339
|
+
is_primary: boolean;
|
|
340
|
+
column_name: string | null;
|
|
341
|
+
ordinal: number;
|
|
342
|
+
is_desc: boolean;
|
|
343
|
+
predicate: string | null;
|
|
344
|
+
access_method: string;
|
|
345
|
+
indexdef: string;
|
|
346
|
+
}>;
|
|
347
|
+
};
|
|
321
348
|
try {
|
|
322
349
|
rows = await sql<{
|
|
323
350
|
index_name: string;
|
|
324
351
|
is_unique: boolean;
|
|
325
352
|
is_primary: boolean;
|
|
326
|
-
column_name: string;
|
|
353
|
+
column_name: string | null;
|
|
327
354
|
ordinal: number;
|
|
355
|
+
is_desc: boolean;
|
|
356
|
+
predicate: string | null;
|
|
357
|
+
access_method: string;
|
|
358
|
+
indexdef: string;
|
|
328
359
|
}>`
|
|
329
360
|
SELECT i.relname AS index_name,
|
|
330
361
|
ix.indisunique AS is_unique,
|
|
331
362
|
ix.indisprimary AS is_primary,
|
|
332
363
|
a.attname AS column_name,
|
|
333
|
-
|
|
364
|
+
k.ord AS ordinal,
|
|
365
|
+
(COALESCE(opt.option, 0) & 1) = 1 AS is_desc,
|
|
366
|
+
pg_get_expr(ix.indpred, ix.indrelid) AS predicate,
|
|
367
|
+
am.amname AS access_method,
|
|
368
|
+
pg_get_indexdef(ix.indexrelid) AS indexdef
|
|
334
369
|
FROM pg_index ix
|
|
335
370
|
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
336
371
|
JOIN pg_class t ON t.oid = ix.indrelid
|
|
337
372
|
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
338
|
-
JOIN
|
|
373
|
+
JOIN pg_am am ON am.oid = i.relam
|
|
374
|
+
CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord)
|
|
375
|
+
LEFT JOIN LATERAL unnest(ix.indoption) WITH ORDINALITY AS opt(option, oord)
|
|
376
|
+
ON opt.oord = k.ord
|
|
377
|
+
LEFT JOIN pg_attribute a ON a.attrelid = ix.indrelid AND a.attnum = k.attnum
|
|
339
378
|
WHERE n.nspname = ${schema}
|
|
340
379
|
AND t.relname = ${table}
|
|
341
|
-
ORDER BY i.relname,
|
|
380
|
+
ORDER BY i.relname, k.ord
|
|
342
381
|
`.execute(k);
|
|
343
382
|
} catch {
|
|
344
|
-
// pg-mem:
|
|
383
|
+
// pg-mem: unnest WITH ORDINALITY / pg_get_expr unsupported — return empty list.
|
|
345
384
|
return [];
|
|
346
385
|
}
|
|
347
386
|
|
|
348
|
-
const byName = new Map<
|
|
387
|
+
const byName = new Map<
|
|
388
|
+
string,
|
|
389
|
+
{
|
|
390
|
+
isUnique: boolean;
|
|
391
|
+
isPrimary: boolean;
|
|
392
|
+
cols: string[];
|
|
393
|
+
orders: ("asc" | "desc")[];
|
|
394
|
+
predicate: string | null;
|
|
395
|
+
hasExpressionKey: boolean;
|
|
396
|
+
accessMethod: string;
|
|
397
|
+
indexdef: string;
|
|
398
|
+
}
|
|
399
|
+
>();
|
|
349
400
|
for (const r of rows.rows) {
|
|
350
401
|
let entry = byName.get(r.index_name);
|
|
351
402
|
if (!entry) {
|
|
352
|
-
entry = {
|
|
403
|
+
entry = {
|
|
404
|
+
isUnique: r.is_unique,
|
|
405
|
+
isPrimary: r.is_primary,
|
|
406
|
+
cols: [],
|
|
407
|
+
orders: [],
|
|
408
|
+
predicate: r.predicate,
|
|
409
|
+
hasExpressionKey: false,
|
|
410
|
+
accessMethod: r.access_method,
|
|
411
|
+
indexdef: r.indexdef,
|
|
412
|
+
};
|
|
353
413
|
byName.set(r.index_name, entry);
|
|
354
414
|
}
|
|
355
|
-
|
|
415
|
+
if (r.column_name === null) {
|
|
416
|
+
// Expression key (attnum 0) — captured from the index def below.
|
|
417
|
+
entry.hasExpressionKey = true;
|
|
418
|
+
} else {
|
|
419
|
+
entry.cols.push(r.column_name);
|
|
420
|
+
entry.orders.push(r.is_desc ? "desc" : "asc");
|
|
421
|
+
}
|
|
356
422
|
}
|
|
357
423
|
return Array.from(byName.entries())
|
|
358
|
-
.filter(([, v]) => !v.isPrimary)
|
|
359
|
-
.map(([name, v]) =>
|
|
424
|
+
.filter(([, v]) => !v.isPrimary) // PK index excluded — PK lives in TableDescriptor.primaryKey
|
|
425
|
+
.map(([name, v]) => {
|
|
426
|
+
const using = v.accessMethod !== "btree" ? v.accessMethod : undefined;
|
|
427
|
+
if (v.hasExpressionKey) {
|
|
428
|
+
// Functional/expression index: lift the raw key expression out of the
|
|
429
|
+
// index def (between `USING <am> (` and its matching `)`, before WHERE).
|
|
430
|
+
const ix: IndexDescriptor = { name, columns: [], unique: v.isUnique, expr: indexDefKeyExpr(v.indexdef) };
|
|
431
|
+
if (using) ix.using = using;
|
|
432
|
+
if (v.predicate !== null) ix.where = v.predicate;
|
|
433
|
+
return ix;
|
|
434
|
+
}
|
|
435
|
+
const ix: IndexDescriptor = { name, columns: v.cols, unique: v.isUnique };
|
|
436
|
+
if (using) ix.using = using;
|
|
437
|
+
if (v.orders.some((o) => o === "desc")) ix.orders = v.orders;
|
|
438
|
+
if (v.predicate !== null) ix.where = v.predicate;
|
|
439
|
+
return ix;
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Extract the raw key-expression text from a pg index def — the balanced `(...)`
|
|
444
|
+
* after `USING <method> `, stripped of a trailing partial-index `WHERE`. */
|
|
445
|
+
function indexDefKeyExpr(indexdef: string): string {
|
|
446
|
+
const open = indexdef.indexOf("(", indexdef.indexOf(" USING "));
|
|
447
|
+
if (open === -1) return indexdef;
|
|
448
|
+
let depth = 0;
|
|
449
|
+
for (let i = open; i < indexdef.length; i++) {
|
|
450
|
+
if (indexdef[i] === "(") depth++;
|
|
451
|
+
else if (indexdef[i] === ")") {
|
|
452
|
+
depth--;
|
|
453
|
+
if (depth === 0) return indexdef.slice(open + 1, i).trim();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return indexdef.slice(open + 1).trim();
|
|
360
457
|
}
|
|
361
458
|
|
|
362
459
|
async function readPgForeignKeys(k: RawKysely, schema: string, table: string): Promise<FkDescriptor[]> {
|
package/src/types.ts
CHANGED
|
@@ -66,6 +66,31 @@ export interface IndexDescriptor {
|
|
|
66
66
|
name: string;
|
|
67
67
|
columns: string[];
|
|
68
68
|
unique: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Per-column sort direction, positional to `columns`. Omitted (or "asc") = the
|
|
71
|
+
* default ascending order, which Postgres does not render in an index def. A
|
|
72
|
+
* "desc" entry emits `<col> DESC` and is compared against the introspected
|
|
73
|
+
* `pg_index.indoption` DESC bit. Absent ⇒ all-ascending.
|
|
74
|
+
*/
|
|
75
|
+
orders?: ("asc" | "desc")[];
|
|
76
|
+
/**
|
|
77
|
+
* Partial-index predicate (raw SQL, e.g. `delivered_at IS NULL`). Emitted as a
|
|
78
|
+
* trailing `WHERE (<predicate>)`; compared against the introspected
|
|
79
|
+
* `pg_get_expr(indpred, …)` after expression normalization. Absent ⇒ full index.
|
|
80
|
+
*/
|
|
81
|
+
where?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Raw key EXPRESSION for a functional/expression index (e.g. `lower((email)::text)`).
|
|
84
|
+
* When present, the index key is this expression and `columns` is empty —
|
|
85
|
+
* emitted as `(<expr>)` and compared against the introspected `pg_get_indexdef`
|
|
86
|
+
* key after normalization.
|
|
87
|
+
*/
|
|
88
|
+
expr?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Index access method (`gin`, `gist`, `hash`, …). Absent / `btree` is the
|
|
91
|
+
* default and not rendered. Emitted as `USING <method>` before the key list.
|
|
92
|
+
*/
|
|
93
|
+
using?: string;
|
|
69
94
|
}
|
|
70
95
|
|
|
71
96
|
export interface CheckDescriptor {
|