okmodel 0.3.0 → 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/README.md +182 -22
- package/dist/contracts/catalog/document.d.ts +6 -2
- package/dist/dialects/pg/ext/citext.js +1 -1
- package/dist/dialects/pg/ext/index.js +1 -1
- package/dist/dialects/pg/ext/pg-trgm.js +1 -1
- package/dist/dialects/pg/index.js +3 -3
- package/dist/dialects/pg/role/sql.d.ts +11 -4
- package/dist/dialects/pg/table.d.ts +17 -1
- package/dist/dialects/pg/view/index.js +1 -1
- package/dist/dialects/pg/view/scratch.d.ts +3 -2
- package/dist/runtime/catalog-cache.d.ts +17 -0
- package/dist/runtime/compat-words.d.ts +23 -0
- package/dist/runtime/drift-detail.d.ts +20 -4
- package/dist/runtime/drift.d.ts +4 -2
- package/dist/runtime/pg/bun.js +2 -2
- package/dist/runtime/pg/pg.js +2 -2
- package/dist/runtime/pg/pglite.js +2 -2
- package/dist/runtime/pg/postgresjs.js +2 -2
- package/dist/runtime/traits/index.js +5 -4
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/validate/index.js +2 -2
- package/dist/shared/2321avmn.js +418 -0
- package/dist/shared/{x6wysh83.js → 3p4b1fg0.js} +3 -5
- package/dist/shared/{wekze34d.js → 41kv2xdm.js} +74 -2
- package/dist/shared/{fs0ex00p.js → 4ef7mmqf.js} +2 -2
- package/dist/shared/5mtf6cj3.js +94 -0
- package/dist/shared/{999ade2q.js → 5vdm4sgp.js} +1 -1
- package/dist/shared/{qybh48nd.js → 6693j9rs.js} +1 -1
- package/dist/shared/{bqha5cg6.js → 6p5k20sm.js} +2 -2
- package/dist/shared/7nk68fj8.js +751 -0
- package/dist/shared/{ne02ng5c.js → 84ptn35q.js} +7 -4
- package/dist/shared/{t616ynfn.js → 8jb4ymh2.js} +59 -12
- package/dist/shared/{th14ygv2.js → 8ktwd3rh.js} +29 -1
- package/dist/shared/9yn6a2vd.js +11 -0
- package/dist/shared/ady1ms74.js +72 -0
- package/dist/shared/{scwxrpx2.js → c5g81ngb.js} +82 -15
- package/dist/shared/{6bazvqwm.js → fb00y8fp.js} +2 -2
- package/dist/shared/{jkc6ws1p.js → g7ez901n.js} +83 -2
- package/dist/shared/{kgnc8grt.js → gdh9mrtp.js} +1 -1
- package/dist/shared/hqjsxnz7.js +13 -0
- package/dist/shared/htnaj77v.js +17 -0
- package/dist/shared/{mt5wswjz.js → jgxzjzcz.js} +1 -1
- package/dist/shared/{6w5j844a.js → jsd07prz.js} +2 -2
- package/dist/shared/{m78s7898.js → kafaa5zv.js} +1 -1
- package/dist/shared/kj680nxv.js +19 -0
- package/dist/shared/{k34e0xf2.js → kkzwp3d2.js} +8 -8
- package/dist/shared/{2ssqrv66.js → n5y9zyj1.js} +1029 -174
- package/dist/shared/{ty3v3hfk.js → nxb42q7h.js} +2 -2
- package/dist/shared/{5nmdenb6.js → q6zsyh31.js} +1003 -40
- package/dist/shared/{fptpmvxp.js → q795afwa.js} +26 -21
- package/dist/shared/{3ekj9x92.js → qn0srwwq.js} +1 -1
- package/dist/shared/s4mb6a4b.js +545 -0
- package/dist/shared/{01yr20s5.js → sf8m6tfy.js} +2 -2
- package/dist/shared/{bryqk9k9.js → sr5ms7b3.js} +1 -1
- package/dist/shared/{meck53jv.js → tny2t3sm.js} +6 -5
- package/dist/shared/{p24x2334.js → v7tpnhnz.js} +1 -1
- package/dist/shared/{xw1d69vd.js → wqem4jed.js} +6 -6
- package/dist/shared/{49ypnfwt.js → xghkt7a9.js} +50 -28
- package/dist/shared/{7fvtrkwr.js → ycatkxmq.js} +1 -1
- package/dist/shared/ygem90rn.js +900 -0
- package/dist/shared/{1tfdfvhh.js → yjw02enp.js} +6 -4
- package/dist/shared/{ydaz1sv3.js → zg24x0hm.js} +1 -1
- package/dist/tooling/cli.js +46 -17
- package/dist/tooling/migrate/apply.d.ts +59 -5
- package/dist/tooling/migrate/backfill.d.ts +136 -0
- package/dist/tooling/migrate/check.d.ts +65 -0
- package/dist/tooling/migrate/classify.d.ts +108 -0
- package/dist/tooling/migrate/commands.d.ts +2 -2
- package/dist/tooling/migrate/config.d.ts +22 -0
- package/dist/tooling/migrate/doctor.d.ts +3 -0
- package/dist/tooling/migrate/drift.d.ts +18 -2
- package/dist/tooling/migrate/estimate.d.ts +50 -0
- package/dist/tooling/migrate/extensions.d.ts +2 -0
- package/dist/tooling/migrate/files.d.ts +12 -0
- package/dist/tooling/migrate/index.d.ts +3 -2
- package/dist/tooling/migrate/index.js +67 -1
- package/dist/tooling/migrate/lint-rules.d.ts +51 -0
- package/dist/tooling/migrate/lint.d.ts +71 -0
- package/dist/tooling/migrate/managed.d.ts +1 -1
- package/dist/tooling/migrate/plan.d.ts +64 -9
- package/dist/tooling/migrate/project.d.ts +42 -7
- package/dist/tooling/migrate/provision.d.ts +18 -0
- package/dist/tooling/migrate/reference.d.ts +40 -0
- package/dist/tooling/migrate/runner.d.ts +108 -0
- package/dist/tooling/migrate/safe.d.ts +77 -0
- package/dist/tooling/migrate/seed.d.ts +20 -0
- package/dist/tooling/migrate/snapshot.d.ts +78 -0
- package/dist/tooling/migrate/status.d.ts +25 -6
- package/dist/tooling/testing/factories.d.ts +117 -0
- package/dist/tooling/testing/facts.d.ts +72 -0
- package/dist/tooling/testing/index.d.ts +89 -0
- package/dist/tooling/testing/index.js +8 -0
- package/dist/tooling/testing/isolation.d.ts +30 -0
- package/dist/tooling/testing/random.d.ts +40 -0
- package/dist/tooling/testing/record.d.ts +38 -0
- package/package.json +6 -2
- package/dist/shared/dbjej1tw.js +0 -456
- package/dist/shared/g4f5kcx9.js +0 -37
- package/dist/shared/qbxzvwwc.js +0 -88
- package/dist/shared/zwmqxb0g.js +0 -79
- package/dist/shared/{xva8v7qk.js → awrsqrn7.js} +3 -3
- package/dist/shared/{75xkjv0t.js → fa9vt245.js} +0 -0
- package/dist/shared/{m9tv7xn7.js → radd3z4g.js} +3 -3
- /package/dist/shared/{0jc3ffzf.js → zvbjc949.js} +0 -0
|
@@ -9,13 +9,20 @@ import {
|
|
|
9
9
|
serializeCatalog2,
|
|
10
10
|
catalogHash2,
|
|
11
11
|
parseCatalog2
|
|
12
|
-
} from "./
|
|
12
|
+
} from "./8jb4ymh2.js";
|
|
13
13
|
import {
|
|
14
|
+
open2
|
|
15
|
+
} from "./wqem4jed.js";
|
|
16
|
+
import {
|
|
17
|
+
canonicalTypeName,
|
|
18
|
+
batchSizeField,
|
|
19
|
+
aboutBatchCount,
|
|
14
20
|
parseReplace,
|
|
15
21
|
planMigration,
|
|
16
22
|
formatPlan,
|
|
23
|
+
parsePlan,
|
|
17
24
|
staleRenames
|
|
18
|
-
} from "./
|
|
25
|
+
} from "./n5y9zyj1.js";
|
|
19
26
|
import {
|
|
20
27
|
contributedFields
|
|
21
28
|
} from "./n0dgs0pb.js";
|
|
@@ -24,12 +31,13 @@ import {
|
|
|
24
31
|
writeWouldValidate
|
|
25
32
|
} from "./9m80mkac.js";
|
|
26
33
|
import {
|
|
27
|
-
catalog
|
|
28
|
-
|
|
34
|
+
catalog,
|
|
35
|
+
isDomain
|
|
36
|
+
} from "./g7ez901n.js";
|
|
29
37
|
import {
|
|
30
38
|
snakeCase,
|
|
31
39
|
emittedTypeName
|
|
32
|
-
} from "./
|
|
40
|
+
} from "./jsd07prz.js";
|
|
33
41
|
import {
|
|
34
42
|
ColumnBuilder2
|
|
35
43
|
} from "./c9z5g0fm.js";
|
|
@@ -243,8 +251,62 @@ var ERROR_DOCS = [
|
|
|
243
251
|
{
|
|
244
252
|
code: "OKM1510",
|
|
245
253
|
title: "Unsafe migration",
|
|
246
|
-
summary: "A migration
|
|
247
|
-
fix: "
|
|
254
|
+
summary: "A migration has an unresolved lint error, or an override with an empty reason or a code the statement did not trigger.",
|
|
255
|
+
fix: "Put `-- okm-allow OKM15xx: reason` on the line above the statement. The code must be one that statement triggered, and the reason must not be empty."
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
code: "OKM1511",
|
|
259
|
+
title: "Drop table",
|
|
260
|
+
summary: "A statement drops a table. The rows and anything that reads them are gone.",
|
|
261
|
+
fix: "Stop reading the table in an expand migration, then drop it in a later contract. Or allow OKM1511 with a reason."
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
code: "OKM1512",
|
|
265
|
+
title: "Drop column",
|
|
266
|
+
summary: "A statement drops a column. Stored values and the code that reads them are gone.",
|
|
267
|
+
fix: "Stop reading the column in an expand migration, then drop it in a later contract. Or allow OKM1512 with a reason."
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
code: "OKM1513",
|
|
271
|
+
title: "Drop enum",
|
|
272
|
+
summary: "A statement drops an enum that columns or code may still depend on.",
|
|
273
|
+
fix: "Move every column off the enum first. Or allow OKM1513 with a reason."
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
code: "OKM1514",
|
|
277
|
+
title: "Drop domain",
|
|
278
|
+
summary: "A statement drops a domain that columns or code may still depend on.",
|
|
279
|
+
fix: "Move every column off the domain first. Or allow OKM1514 with a reason."
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
code: "OKM1515",
|
|
283
|
+
title: "Drop function",
|
|
284
|
+
summary: "A statement drops a function that queries or triggers may still call.",
|
|
285
|
+
fix: "Remove the callers in an expand migration, then drop the function. Or allow OKM1515 with a reason."
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
code: "OKM1516",
|
|
289
|
+
title: "Drop view",
|
|
290
|
+
summary: "A statement drops a view that queries may still read.",
|
|
291
|
+
fix: "Stop reading the view in an expand migration, then drop it. Or allow OKM1516 with a reason."
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
code: "OKM1517",
|
|
295
|
+
title: "Drop extension",
|
|
296
|
+
summary: "A statement drops an extension. A dependent still in the catalog is refused before this finding.",
|
|
297
|
+
fix: "Confirm nothing outside the catalog uses the extension. Or allow OKM1517 with a reason."
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
code: "OKM1518",
|
|
301
|
+
title: "Drop materialized view",
|
|
302
|
+
summary: "A statement drops a materialized view and the rows it stores.",
|
|
303
|
+
fix: "Stop reading the view in an expand migration, then drop it. Or allow OKM1518 with a reason."
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
code: "OKM1519",
|
|
307
|
+
title: "Rename column",
|
|
308
|
+
summary: "A statement renames a column. Existing queries still use the old name.",
|
|
309
|
+
fix: "Add the new column, backfill, and switch readers before dropping the old name. Or allow OKM1519 with a reason."
|
|
248
310
|
},
|
|
249
311
|
{
|
|
250
312
|
code: "OKM1520",
|
|
@@ -256,7 +318,7 @@ var ERROR_DOCS = [
|
|
|
256
318
|
code: "OKM1521",
|
|
257
319
|
title: "Snapshot mismatch",
|
|
258
320
|
summary: "Provisioning from the snapshot does not match the replayed migration history.",
|
|
259
|
-
fix: "Regenerate the snapshot from the history. okm migrate check is the command that reports this."
|
|
321
|
+
fix: "Regenerate the snapshot from the history. okm migrate check --provision is the command that reports this."
|
|
260
322
|
},
|
|
261
323
|
{
|
|
262
324
|
code: "OKM1522",
|
|
@@ -264,12 +326,114 @@ var ERROR_DOCS = [
|
|
|
264
326
|
summary: "Another apply already holds the target's migration lock.",
|
|
265
327
|
fix: "Wait for that apply to finish. Do not start a second apply against the same target."
|
|
266
328
|
},
|
|
329
|
+
{
|
|
330
|
+
code: "OKM1523",
|
|
331
|
+
title: "Rename table",
|
|
332
|
+
summary: "A statement renames a table. Existing queries still use the old name.",
|
|
333
|
+
fix: "Create the new table, backfill, and switch readers before dropping the old name. Or allow OKM1523 with a reason."
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
code: "OKM1524",
|
|
337
|
+
title: "Column type change",
|
|
338
|
+
summary: "A statement changes a column type. Existing readers and writers may not accept the new type.",
|
|
339
|
+
fix: "Add a new column, backfill, and switch readers before dropping the old one. Or allow OKM1524 with a reason."
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
code: "OKM1525",
|
|
343
|
+
title: "Required column",
|
|
344
|
+
summary: "A statement adds a NOT NULL column with no default to a table that already has rows.",
|
|
345
|
+
fix: "Add the column nullable, backfill, then set it NOT NULL. Or allow OKM1525 with a reason."
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
code: "OKM1526",
|
|
349
|
+
title: "Drop default",
|
|
350
|
+
summary: "A statement removes a column default. Writers that omitted the column will fail.",
|
|
351
|
+
fix: "Update writers to send the column, then drop the default. Or allow OKM1526 with a reason."
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
code: "OKM1527",
|
|
355
|
+
title: "Shrink length",
|
|
356
|
+
summary: "A statement shortens a character length. Values longer than the new limit no longer fit.",
|
|
357
|
+
fix: "Shorten the stored values first, or keep the old length. Or allow OKM1527 with a reason."
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
code: "OKM1528",
|
|
361
|
+
title: "Unique constraint",
|
|
362
|
+
summary: "A statement adds a unique or primary-key constraint on a table that already has rows.",
|
|
363
|
+
fix: "Deduplicate the rows first. Or allow OKM1528 with a reason."
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
code: "OKM1529",
|
|
367
|
+
title: "Unique index",
|
|
368
|
+
summary: "A statement creates a unique index on a table that already has rows.",
|
|
369
|
+
fix: "Deduplicate the rows first. Or allow OKM1529 with a reason."
|
|
370
|
+
},
|
|
267
371
|
{
|
|
268
372
|
code: "OKM1530",
|
|
269
373
|
title: "Ambiguous rename",
|
|
270
374
|
summary: "A rename could match more than one object.",
|
|
271
375
|
fix: "Declare the rename explicitly so the plan has one pairing."
|
|
272
376
|
},
|
|
377
|
+
{
|
|
378
|
+
code: "OKM1531",
|
|
379
|
+
title: "Validating check",
|
|
380
|
+
summary: "A statement adds or validates a check against rows already stored.",
|
|
381
|
+
fix: "Add the check NOT VALID, repair the rows, then validate it. Or allow OKM1531 with a reason."
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
code: "OKM1532",
|
|
385
|
+
title: "Validating foreign key",
|
|
386
|
+
summary: "A statement adds or validates a foreign key against rows already stored.",
|
|
387
|
+
fix: "Add the foreign key NOT VALID, repair the rows, then validate it. Or allow OKM1532 with a reason."
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
code: "OKM1533",
|
|
391
|
+
title: "Narrowing type",
|
|
392
|
+
summary: "A statement narrows a numeric or integer type. Values outside the new range no longer fit.",
|
|
393
|
+
fix: "Rewrite the values into the new range first. Or allow OKM1533 with a reason."
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
code: "OKM1534",
|
|
397
|
+
title: "Index locks",
|
|
398
|
+
summary: "A statement creates an index on an existing table without CONCURRENTLY.",
|
|
399
|
+
fix: "Create the index with CREATE INDEX CONCURRENTLY, outside a transaction. The planner does this on an existing table."
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
code: "OKM1535",
|
|
403
|
+
title: "Check without NOT VALID",
|
|
404
|
+
summary: "A statement adds a check on an existing table without NOT VALID.",
|
|
405
|
+
fix: "Add the check NOT VALID, then validate it in its own step. The planner does this on an existing table."
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
code: "OKM1536",
|
|
409
|
+
title: "Foreign key without NOT VALID",
|
|
410
|
+
summary: "A statement adds a foreign key on an existing table without NOT VALID.",
|
|
411
|
+
fix: "Add the foreign key NOT VALID, then validate it in its own step. The planner does this on an existing table."
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
code: "OKM1537",
|
|
415
|
+
title: "Set not null",
|
|
416
|
+
summary: "A statement sets NOT NULL on a column that already exists.",
|
|
417
|
+
fix: "Add CHECK (column IS NOT NULL) NOT VALID, validate it, then SET NOT NULL and drop the check. The planner does this on an existing column."
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
code: "OKM1538",
|
|
421
|
+
title: "Type rewrite",
|
|
422
|
+
summary: "A statement changes a column type in a way that rewrites the table.",
|
|
423
|
+
fix: "There is no safe form for a rewriting type change in this version. A column swap is expand and contract work for a later step. OKM1524 already requires a reason for the type change."
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
code: "OKM1539",
|
|
427
|
+
title: "Timestamp without time zone",
|
|
428
|
+
summary: "A column is timestamp without time zone. The instant depends on the session time zone.",
|
|
429
|
+
fix: "Use timestamptz."
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
code: "OKM1540",
|
|
433
|
+
title: "varchar length",
|
|
434
|
+
summary: "A column is varchar(n). text stores the same values without a length limit in the type.",
|
|
435
|
+
fix: "Use text, and check the length in the application when one is required."
|
|
436
|
+
},
|
|
273
437
|
{
|
|
274
438
|
code: "OKM1541",
|
|
275
439
|
title: "Picklist removal",
|
|
@@ -282,6 +446,48 @@ var ERROR_DOCS = [
|
|
|
282
446
|
summary: "A migration contains a data statement outside backfill().",
|
|
283
447
|
fix: "Move the statement into backfill(). Data changes are batched and resumable there."
|
|
284
448
|
},
|
|
449
|
+
{
|
|
450
|
+
code: "OKM1543",
|
|
451
|
+
title: "serial column",
|
|
452
|
+
summary: "A column uses serial or a nextval default. Identity is the form this repo generates.",
|
|
453
|
+
fix: "Use an identity column generated always."
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
code: "OKM1544",
|
|
457
|
+
title: "json column",
|
|
458
|
+
summary: "A column is json. jsonb is available and stores the same documents in binary.",
|
|
459
|
+
fix: "Use jsonb."
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
code: "OKM1545",
|
|
463
|
+
title: "Identity not always",
|
|
464
|
+
summary: "An identity column is generated by default. The repo default is generated always.",
|
|
465
|
+
fix: "Declare the identity as generated always."
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
code: "OKM1546",
|
|
469
|
+
title: "Backfill key",
|
|
470
|
+
summary: "A backfill step targets a table that has no primary key.",
|
|
471
|
+
fix: "Add a primary key. A backfill walks that key and does not scan the table."
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
code: "OKM1547",
|
|
475
|
+
title: "History differs",
|
|
476
|
+
summary: "Replaying a migration does not match its stored catalog, or the file was generated from a different parent than the previous migration.",
|
|
477
|
+
fix: "Regenerate the migration from the previous catalog so the SQL and the stored catalog describe the same schema."
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
code: "OKM1548",
|
|
481
|
+
title: "Previous catalog",
|
|
482
|
+
summary: "An expand migration drops or changes a table, column, constraint, or type the previous catalog relies on.",
|
|
483
|
+
fix: "Keep every table, column, constraint, and type the previous catalog relies on, or classify the migration as contract."
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
code: "OKM1549",
|
|
487
|
+
title: "Stale head",
|
|
488
|
+
summary: "The last migration's catalog does not match the schema.",
|
|
489
|
+
fix: "Run okm generate."
|
|
490
|
+
},
|
|
285
491
|
{
|
|
286
492
|
code: "OKM1601",
|
|
287
493
|
title: "Stale typed SQL",
|
|
@@ -494,6 +700,532 @@ function indexDocs() {
|
|
|
494
700
|
return index;
|
|
495
701
|
}
|
|
496
702
|
|
|
703
|
+
// src/tooling/migrate/lint.ts
|
|
704
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
705
|
+
import { join } from "node:path";
|
|
706
|
+
|
|
707
|
+
// src/tooling/migrate/lint-rules.ts
|
|
708
|
+
var INTEGER_WIDTH = {
|
|
709
|
+
smallint: 2,
|
|
710
|
+
integer: 4,
|
|
711
|
+
bigint: 8
|
|
712
|
+
};
|
|
713
|
+
var FLOAT_WIDTH = {
|
|
714
|
+
real: 4,
|
|
715
|
+
"double precision": 8
|
|
716
|
+
};
|
|
717
|
+
var STEP_RULES = [
|
|
718
|
+
rule("OKM1511", "destructive", "error", "Dropping a table destroys its rows.", (input) => hit(matches(input.step, "drop-table", /^drop table\b/), "drops a table")),
|
|
719
|
+
rule("OKM1512", "destructive", "error", "Dropping a column destroys its values.", (input) => hit(matches(input.step, "drop-column", /^alter table\s+\S+\s+drop column\b/), "drops a column")),
|
|
720
|
+
rule("OKM1513", "destructive", "error", "Dropping an enum can break columns that use it.", (input) => hit(droppedType(input) === "enum", "drops an enum")),
|
|
721
|
+
rule("OKM1514", "destructive", "error", "Dropping a domain can break columns that use it.", (input) => hit(droppedType(input) === "domain", "drops a domain")),
|
|
722
|
+
rule("OKM1515", "destructive", "error", "Dropping a function can break its callers.", (input) => hit(matches(input.step, "drop-function", /^drop function\b/), "drops a function")),
|
|
723
|
+
rule("OKM1516", "destructive", "error", "Dropping a view can break queries that read it.", (input) => hit(matches(input.step, "drop-view", /^drop view\b/), "drops a view")),
|
|
724
|
+
rule("OKM1517", "destructive", "error", "Dropping an extension removes it even when the catalog has no dependent.", (input) => hit(matches(input.step, "drop-extension", /^drop extension\b/), "drops an extension")),
|
|
725
|
+
rule("OKM1518", "destructive", "error", "Dropping a materialized view destroys the rows it stores.", (input) => hit(matches(input.step, "drop-matview", /^drop materialized view\b/), "drops a materialized view")),
|
|
726
|
+
rule("OKM1519", "backward-incompatible", "error", "Renaming a column breaks queries that still use the old name.", (input) => hit(matches(input.step, "rename-column", /^alter table\s+\S+\s+rename column\b/), "renames a column")),
|
|
727
|
+
rule("OKM1523", "backward-incompatible", "error", "Renaming a table breaks queries that still use the old name.", (input) => hit(renamesTable(input.step), "renames a table")),
|
|
728
|
+
rule("OKM1524", "backward-incompatible", "error", "Changing a column type can break readers and writers.", (input) => typeChange(input, "incompatible")),
|
|
729
|
+
rule("OKM1525", "backward-incompatible", "error", "Adding NOT NULL without a default fails when the table already has rows.", (input) => hit(requiredColumn(input), "adds NOT NULL without a default")),
|
|
730
|
+
rule("OKM1526", "backward-incompatible", "error", "Removing a default breaks writers that omit the column.", (input) => hit(matches(input.step, "drop-default", /^alter table\s+\S+\s+alter column\s+\S+\s+drop default\b/), "removes a default")),
|
|
731
|
+
rule("OKM1527", "backward-incompatible", "error", "Shortening a character length rejects values that used to fit.", (input) => typeChange(input, "shrink")),
|
|
732
|
+
rule("OKM1528", "data-dependent", "error", "A new unique or primary key on an existing table fails when rows collide.", (input) => hit(uniqueConstraint(input), "builds a unique or primary key")),
|
|
733
|
+
rule("OKM1529", "data-dependent", "error", "A new unique index on an existing table fails when rows collide.", (input) => hit(uniqueIndex(input), "creates a unique index")),
|
|
734
|
+
rule("OKM1531", "data-dependent", "error", "A check that validates existing rows fails when a row does not pass.", (input) => hit(validatingCheck(input), "validates a check")),
|
|
735
|
+
rule("OKM1532", "data-dependent", "error", "A foreign key that validates existing rows fails when a row has no target.", (input) => hit(validatingForeignKey(input), "validates a foreign key")),
|
|
736
|
+
rule("OKM1533", "data-dependent", "error", "Narrowing an integer, float, or numeric type rejects values outside the new range.", (input) => typeChange(input, "narrow")),
|
|
737
|
+
rule("OKM1534", "locking", "error", "Creating an index on an existing table without CONCURRENTLY locks writes.", (input) => hit(blockingIndex(input), "creates an index without CONCURRENTLY")),
|
|
738
|
+
rule("OKM1535", "locking", "error", "Adding a check without NOT VALID locks the table while it scans.", (input) => hit(checkWithoutNotValid(input), "adds a check without NOT VALID")),
|
|
739
|
+
rule("OKM1536", "locking", "error", "Adding a foreign key without NOT VALID locks the table while it scans.", (input) => hit(foreignKeyWithoutNotValid(input), "adds a foreign key without NOT VALID")),
|
|
740
|
+
rule("OKM1537", "locking", "error", "SET NOT NULL locks the table while it scans for nulls.", (input) => hit(unsafeSetNotNull(input), "sets NOT NULL")),
|
|
741
|
+
rule("OKM1538", "locking", "warning", "A type change that rewrites the table locks it for the rewrite.", (input) => typeChange(input, "rewrite")),
|
|
742
|
+
rule("OKM1542", "data-dependent", "error", "A data statement outside backfill() is not a migration step.", (input) => hit(dataStatement(input.step), "changes rows outside backfill()"))
|
|
743
|
+
];
|
|
744
|
+
var COLUMN_RULES = [
|
|
745
|
+
{
|
|
746
|
+
code: "OKM1539",
|
|
747
|
+
category: "type-preference",
|
|
748
|
+
severity: "warning",
|
|
749
|
+
doc: "timestamp without time zone depends on the session time zone; use timestamptz.",
|
|
750
|
+
check: (column) => /^timestamp(?:\(\d+\))? without time zone$/.test(canonical(column.definition.dataType)) ? "is timestamp without time zone" : undefined
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
code: "OKM1540",
|
|
754
|
+
category: "type-preference",
|
|
755
|
+
severity: "warning",
|
|
756
|
+
doc: "varchar(n) can be text; a length check belongs in the application.",
|
|
757
|
+
check: (column) => /^character varying\(\d+\)$/.test(canonical(column.definition.dataType)) ? "is varchar(n)" : undefined
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
code: "OKM1543",
|
|
761
|
+
category: "type-preference",
|
|
762
|
+
severity: "warning",
|
|
763
|
+
doc: "serial and nextval defaults should be identity generated always.",
|
|
764
|
+
check: (column) => isSerial(column) ? "uses serial or nextval" : undefined
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
code: "OKM1544",
|
|
768
|
+
category: "type-preference",
|
|
769
|
+
severity: "warning",
|
|
770
|
+
doc: "json should be jsonb, which is available on every supported Postgres.",
|
|
771
|
+
check: (column) => canonical(column.definition.dataType) === "json" ? "is json" : undefined
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
code: "OKM1545",
|
|
775
|
+
category: "type-preference",
|
|
776
|
+
severity: "warning",
|
|
777
|
+
doc: "An identity that is not generated always differs from the repo default.",
|
|
778
|
+
check: (column) => column.definition.identity?.always === false ? "is generated by default" : undefined
|
|
779
|
+
}
|
|
780
|
+
];
|
|
781
|
+
function rule(code, category, severity, doc, check) {
|
|
782
|
+
return { code, category, severity, doc, check };
|
|
783
|
+
}
|
|
784
|
+
function hit(matched, reason) {
|
|
785
|
+
return matched ? [reason] : [];
|
|
786
|
+
}
|
|
787
|
+
function normalized(sql) {
|
|
788
|
+
const collapsed = sql.trim().replace(/\s+/g, " ");
|
|
789
|
+
let out = "";
|
|
790
|
+
let quoted = false;
|
|
791
|
+
for (const char of collapsed) {
|
|
792
|
+
if (char === '"') {
|
|
793
|
+
quoted = !quoted;
|
|
794
|
+
out += char;
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
out += quoted ? char : char.toLowerCase();
|
|
798
|
+
}
|
|
799
|
+
return out;
|
|
800
|
+
}
|
|
801
|
+
function dataStatement(step) {
|
|
802
|
+
if (step.backfill !== undefined)
|
|
803
|
+
return false;
|
|
804
|
+
const text = normalized(step.sql);
|
|
805
|
+
if (/^(?:insert|update|delete|merge|truncate)\b/.test(text))
|
|
806
|
+
return true;
|
|
807
|
+
return /^with\b/.test(text) && /\b(?:insert|update|delete|merge)\b/.test(text);
|
|
808
|
+
}
|
|
809
|
+
function matches(step, kind, pattern) {
|
|
810
|
+
return pattern.test(normalized(step.sql)) || step.kind === kind;
|
|
811
|
+
}
|
|
812
|
+
function renamesTable(step) {
|
|
813
|
+
const text = normalized(step.sql);
|
|
814
|
+
if (/^alter table\s+\S+\s+rename to\b/.test(text))
|
|
815
|
+
return true;
|
|
816
|
+
return step.kind === "rename-table" && !/^alter table\s+\S+\s+rename (column|constraint)\b/.test(text);
|
|
817
|
+
}
|
|
818
|
+
function droppedType(input) {
|
|
819
|
+
const text = normalized(input.step.sql);
|
|
820
|
+
const dropping = /^drop type\b/.test(text);
|
|
821
|
+
if (!dropping && input.step.kind !== "drop-enum" && input.step.kind !== "drop-domain") {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (input.step.kind === "drop-domain")
|
|
825
|
+
return "domain";
|
|
826
|
+
if (input.step.kind === "drop-enum")
|
|
827
|
+
return "enum";
|
|
828
|
+
const name = lastName(/^drop type\s+(\S+)/.exec(text)?.[1]);
|
|
829
|
+
const found = input.before.objects.find((object) => object.kind === "type" && object.identity.name === name);
|
|
830
|
+
if (found !== undefined && isDomain(found.definition))
|
|
831
|
+
return "domain";
|
|
832
|
+
return "enum";
|
|
833
|
+
}
|
|
834
|
+
function requiredColumn(input) {
|
|
835
|
+
const text = normalized(input.step.sql);
|
|
836
|
+
const adding = /^alter table\s+\S+\s+add column\b/.test(text) || input.step.kind === "add-column-required";
|
|
837
|
+
if (!adding)
|
|
838
|
+
return false;
|
|
839
|
+
if (input.step.kind === "add-column")
|
|
840
|
+
return false;
|
|
841
|
+
if (!tableExists(input))
|
|
842
|
+
return false;
|
|
843
|
+
if (input.step.kind === "add-column-required")
|
|
844
|
+
return true;
|
|
845
|
+
const body = text.replace(/^alter table\s+\S+\s+add column\s+/, "");
|
|
846
|
+
return /\bnot null\b/.test(body) && !/\bdefault\b/.test(body) && !/\bgenerated\b/.test(body);
|
|
847
|
+
}
|
|
848
|
+
function uniqueConstraint(input) {
|
|
849
|
+
if (!tableExists(input))
|
|
850
|
+
return false;
|
|
851
|
+
const text = normalized(input.step.sql);
|
|
852
|
+
if (/\busing index\b/.test(text))
|
|
853
|
+
return false;
|
|
854
|
+
if (/^alter table\s+\S+\s+add constraint\b/.test(text) && /\b(unique|primary key)\b/.test(text)) {
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
if (!/^create unique index\b/.test(text))
|
|
858
|
+
return false;
|
|
859
|
+
const name = createdIndexName(text);
|
|
860
|
+
if (name === undefined)
|
|
861
|
+
return false;
|
|
862
|
+
return input.later.some((step) => usesIndex(normalized(step.sql), name));
|
|
863
|
+
}
|
|
864
|
+
function uniqueIndex(input) {
|
|
865
|
+
const text = normalized(input.step.sql);
|
|
866
|
+
if (!/^create unique index\b/.test(text))
|
|
867
|
+
return false;
|
|
868
|
+
if (!tableExists(input))
|
|
869
|
+
return false;
|
|
870
|
+
const name = createdIndexName(text);
|
|
871
|
+
if (name !== undefined && input.later.some((step) => usesIndex(normalized(step.sql), name))) {
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
function createdIndexName(text) {
|
|
877
|
+
const match = /^create unique index (?:concurrently )?(?:"([^"]+)"|([a-z_][\w$]*))/.exec(text);
|
|
878
|
+
return match?.[1] ?? match?.[2];
|
|
879
|
+
}
|
|
880
|
+
function usesIndex(text, name) {
|
|
881
|
+
return text.includes(`using index "${name}"`) || text.includes(`using index ${name}`);
|
|
882
|
+
}
|
|
883
|
+
function unsafeSetNotNull(input) {
|
|
884
|
+
const text = normalized(input.step.sql);
|
|
885
|
+
const match = /^alter table\s+(\S+)\s+alter column\s+(\S+)\s+set not null\b/.exec(text);
|
|
886
|
+
if (match === null && input.step.kind !== "set-not-null")
|
|
887
|
+
return false;
|
|
888
|
+
if (!tableExists(input))
|
|
889
|
+
return false;
|
|
890
|
+
const table = match?.[1];
|
|
891
|
+
const column = match?.[2];
|
|
892
|
+
if (table !== undefined && column !== undefined && validatedNotNullCheck(input.earlier, table, column)) {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
function validatedNotNullCheck(earlier, table, column) {
|
|
898
|
+
const needle = `check (${column} is not null) not valid`;
|
|
899
|
+
for (const step of earlier) {
|
|
900
|
+
const text = normalized(step.sql);
|
|
901
|
+
if (!text.startsWith(`alter table ${table} `) || !text.includes(needle))
|
|
902
|
+
continue;
|
|
903
|
+
const added = /^alter table\s+\S+\s+add constraint\s+(\S+)\s+check\b/.exec(text);
|
|
904
|
+
const name = added?.[1];
|
|
905
|
+
if (name === undefined)
|
|
906
|
+
continue;
|
|
907
|
+
const validated = earlier.some((item) => normalized(item.sql).startsWith(`alter table ${table} validate constraint ${name}`));
|
|
908
|
+
if (validated)
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
function blockingIndex(input) {
|
|
914
|
+
const text = normalized(input.step.sql);
|
|
915
|
+
if (!/^create(?: unique)? index\b/.test(text) && input.step.kind !== "create-index")
|
|
916
|
+
return false;
|
|
917
|
+
if (/\bconcurrently\b/.test(text))
|
|
918
|
+
return false;
|
|
919
|
+
return tableExists(input);
|
|
920
|
+
}
|
|
921
|
+
function checkWithoutNotValid(input) {
|
|
922
|
+
return addedConstraint(input, "check") && !/\bnot valid\b/.test(normalized(input.step.sql));
|
|
923
|
+
}
|
|
924
|
+
function foreignKeyWithoutNotValid(input) {
|
|
925
|
+
return addedConstraint(input, "foreign key") && !/\bnot valid\b/.test(normalized(input.step.sql));
|
|
926
|
+
}
|
|
927
|
+
function validatingCheck(input) {
|
|
928
|
+
if (widenValidate(input))
|
|
929
|
+
return false;
|
|
930
|
+
if (validates(input, "check"))
|
|
931
|
+
return true;
|
|
932
|
+
return checkWithoutNotValid(input);
|
|
933
|
+
}
|
|
934
|
+
function widenValidate(input) {
|
|
935
|
+
const text = normalized(input.step.sql);
|
|
936
|
+
if (!/^alter table\s+\S+\s+validate constraint\b/.test(text))
|
|
937
|
+
return false;
|
|
938
|
+
if (input.step.kind === "widen-check")
|
|
939
|
+
return true;
|
|
940
|
+
const name = /^alter table\s+\S+\s+validate constraint\s+(\S+)/.exec(text)?.[1];
|
|
941
|
+
if (name === undefined)
|
|
942
|
+
return false;
|
|
943
|
+
return input.earlier.some((step) => {
|
|
944
|
+
if (step.kind !== "widen-check")
|
|
945
|
+
return false;
|
|
946
|
+
const prior = normalized(step.sql);
|
|
947
|
+
return prior.includes(`add constraint ${name} `);
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
function validatingForeignKey(input) {
|
|
951
|
+
if (validates(input, "foreignKey"))
|
|
952
|
+
return true;
|
|
953
|
+
return foreignKeyWithoutNotValid(input);
|
|
954
|
+
}
|
|
955
|
+
function addedConstraint(input, kind) {
|
|
956
|
+
if (!tableExists(input))
|
|
957
|
+
return false;
|
|
958
|
+
const text = normalized(input.step.sql);
|
|
959
|
+
if (!/^alter table\s+\S+\s+add constraint\b/.test(text))
|
|
960
|
+
return false;
|
|
961
|
+
return new RegExp(`\\b${kind}\\b`).test(text);
|
|
962
|
+
}
|
|
963
|
+
function validates(input, kind) {
|
|
964
|
+
const text = normalized(input.step.sql);
|
|
965
|
+
const table = /^alter table\s+\S+\s+validate constraint\s+(\S+)/.exec(text);
|
|
966
|
+
if (table !== null) {
|
|
967
|
+
const token = table[1];
|
|
968
|
+
if (token === undefined)
|
|
969
|
+
return kind === "check";
|
|
970
|
+
const name = lastName(token);
|
|
971
|
+
if (name === undefined)
|
|
972
|
+
return kind === "check";
|
|
973
|
+
const constraint = findConstraint(input.after, name);
|
|
974
|
+
if (constraint === undefined)
|
|
975
|
+
return kind === "check";
|
|
976
|
+
return constraint.definition.constraintKind === kind;
|
|
977
|
+
}
|
|
978
|
+
if (kind === "check" && /^alter domain\s+\S+\s+validate constraint\b/.test(text))
|
|
979
|
+
return true;
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
function typeChange(input, mode) {
|
|
983
|
+
const text = normalized(input.step.sql);
|
|
984
|
+
const changing = /^alter table\s+\S+\s+alter column\s+\S+\s+set data type\b/.test(text) || input.step.kind === "set-column-type";
|
|
985
|
+
if (!changing)
|
|
986
|
+
return [];
|
|
987
|
+
const compared = columnTypes(input);
|
|
988
|
+
if (compared === undefined) {
|
|
989
|
+
if (mode === "incompatible" || mode === "rewrite")
|
|
990
|
+
return ["changes a column type"];
|
|
991
|
+
return [];
|
|
992
|
+
}
|
|
993
|
+
const { before, after } = compared;
|
|
994
|
+
if (before === after)
|
|
995
|
+
return [];
|
|
996
|
+
const shrink = lengthShrink(before, after);
|
|
997
|
+
if (mode === "shrink")
|
|
998
|
+
return shrink ? ["shortens a character length"] : [];
|
|
999
|
+
if (shrink)
|
|
1000
|
+
return [];
|
|
1001
|
+
if (mode === "narrow")
|
|
1002
|
+
return narrows(before, after) ? ["narrows a type"] : [];
|
|
1003
|
+
if (lengthGrow(before, after))
|
|
1004
|
+
return [];
|
|
1005
|
+
if (mode === "incompatible")
|
|
1006
|
+
return ["changes a column type"];
|
|
1007
|
+
return rewrites(before, after) ? ["rewrites the table"] : [];
|
|
1008
|
+
}
|
|
1009
|
+
function columnTypes(input) {
|
|
1010
|
+
const text = normalized(input.step.sql);
|
|
1011
|
+
const match = /^alter table\s+(\S+)\s+alter column\s+(\S+)\s+set data type\b/.exec(text);
|
|
1012
|
+
const table = lastName(match?.[1]);
|
|
1013
|
+
const column = lastName(match?.[2]);
|
|
1014
|
+
if (table === undefined || column === undefined)
|
|
1015
|
+
return;
|
|
1016
|
+
const previous = findColumn(input.before, table, column);
|
|
1017
|
+
const next = findColumn(input.after, table, column);
|
|
1018
|
+
if (previous === undefined || next === undefined)
|
|
1019
|
+
return;
|
|
1020
|
+
return {
|
|
1021
|
+
before: canonical(previous.definition.dataType),
|
|
1022
|
+
after: canonical(next.definition.dataType)
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function lengthShrink(before, after) {
|
|
1026
|
+
const left = charLength(before);
|
|
1027
|
+
const right = charLength(after);
|
|
1028
|
+
return left !== undefined && right !== undefined && left.family === right.family && right.length < left.length;
|
|
1029
|
+
}
|
|
1030
|
+
function lengthGrow(before, after) {
|
|
1031
|
+
const left = charLength(before);
|
|
1032
|
+
const right = charLength(after);
|
|
1033
|
+
return left !== undefined && right !== undefined && left.family === right.family && right.length > left.length;
|
|
1034
|
+
}
|
|
1035
|
+
function rewrites(before, after) {
|
|
1036
|
+
if (lengthShrink(before, after) || lengthGrow(before, after))
|
|
1037
|
+
return false;
|
|
1038
|
+
if (isCharacter(before) && after === "text")
|
|
1039
|
+
return false;
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
function narrows(before, after) {
|
|
1043
|
+
const leftInt = INTEGER_WIDTH[before];
|
|
1044
|
+
const rightInt = INTEGER_WIDTH[after];
|
|
1045
|
+
if (leftInt !== undefined && rightInt !== undefined)
|
|
1046
|
+
return rightInt < leftInt;
|
|
1047
|
+
const leftFloat = FLOAT_WIDTH[before];
|
|
1048
|
+
const rightFloat = FLOAT_WIDTH[after];
|
|
1049
|
+
if (leftFloat !== undefined && rightFloat !== undefined)
|
|
1050
|
+
return rightFloat < leftFloat;
|
|
1051
|
+
const left = numericSpec(before);
|
|
1052
|
+
const right = numericSpec(after);
|
|
1053
|
+
if (left === undefined || right === undefined)
|
|
1054
|
+
return false;
|
|
1055
|
+
return right.precision < left.precision || right.scale < left.scale;
|
|
1056
|
+
}
|
|
1057
|
+
function charLength(type) {
|
|
1058
|
+
const varying = /^character varying\((\d+)\)$/.exec(type);
|
|
1059
|
+
if (varying?.[1] !== undefined)
|
|
1060
|
+
return { family: "varying", length: Number(varying[1]) };
|
|
1061
|
+
const fixed = /^character\((\d+)\)$/.exec(type);
|
|
1062
|
+
if (fixed?.[1] !== undefined)
|
|
1063
|
+
return { family: "fixed", length: Number(fixed[1]) };
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
function isCharacter(type) {
|
|
1067
|
+
return type === "text" || charLength(type) !== undefined;
|
|
1068
|
+
}
|
|
1069
|
+
function numericSpec(type) {
|
|
1070
|
+
const match = /^numeric(?:\((\d+)(?:,(\d+))?\))?$/.exec(type);
|
|
1071
|
+
if (match === null)
|
|
1072
|
+
return;
|
|
1073
|
+
if (match[1] === undefined)
|
|
1074
|
+
return { precision: 1000, scale: 0 };
|
|
1075
|
+
return {
|
|
1076
|
+
precision: Number(match[1]),
|
|
1077
|
+
scale: match[2] === undefined ? 0 : Number(match[2])
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function tableExists(input) {
|
|
1081
|
+
const name = statementTable(input.step);
|
|
1082
|
+
return name !== undefined && input.existingTables.has(name);
|
|
1083
|
+
}
|
|
1084
|
+
function statementTable(step) {
|
|
1085
|
+
const text = normalized(step.sql);
|
|
1086
|
+
const altered = /^(?:alter|drop) table\s+(\S+)/.exec(text);
|
|
1087
|
+
if (altered?.[1] !== undefined)
|
|
1088
|
+
return lastName(altered[1]);
|
|
1089
|
+
const index = /^create(?: unique)? index(?: concurrently)?\s+\S+\s+on\s+(\S+)/.exec(text);
|
|
1090
|
+
return lastName(index?.[1]);
|
|
1091
|
+
}
|
|
1092
|
+
function lastName(token) {
|
|
1093
|
+
if (token === undefined)
|
|
1094
|
+
return;
|
|
1095
|
+
const parts = [...token.matchAll(/"(?:[^"]|"")*"|[A-Za-z_][\w$]*/g)];
|
|
1096
|
+
const last = parts.at(-1)?.[0];
|
|
1097
|
+
if (last === undefined)
|
|
1098
|
+
return;
|
|
1099
|
+
if (last.startsWith('"'))
|
|
1100
|
+
return last.slice(1, -1).replaceAll('""', '"');
|
|
1101
|
+
return last;
|
|
1102
|
+
}
|
|
1103
|
+
function findColumn(source, table, name) {
|
|
1104
|
+
return source.objects.find((object) => object.kind === "column" && object.identity.parent.name === table && object.identity.name === name);
|
|
1105
|
+
}
|
|
1106
|
+
function findConstraint(source, name) {
|
|
1107
|
+
return source.objects.find((object) => object.kind === "constraint" && object.identity.name === name);
|
|
1108
|
+
}
|
|
1109
|
+
function canonical(typeName) {
|
|
1110
|
+
return canonicalTypeName(typeName);
|
|
1111
|
+
}
|
|
1112
|
+
function isSerial(column) {
|
|
1113
|
+
const typeName = column.definition.dataType.toLowerCase();
|
|
1114
|
+
if (typeName === "serial" || typeName === "smallserial" || typeName === "bigserial")
|
|
1115
|
+
return true;
|
|
1116
|
+
if (column.definition.identity !== undefined)
|
|
1117
|
+
return false;
|
|
1118
|
+
return /nextval\s*\(/i.test(column.definition.defaultExpression ?? "");
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/tooling/migrate/lint.ts
|
|
1122
|
+
function lintPlan(plan, before, after) {
|
|
1123
|
+
const existingTables = tableNames(before);
|
|
1124
|
+
const findings = [];
|
|
1125
|
+
plan.steps.forEach((step, index) => {
|
|
1126
|
+
findings.push(...lintStep(step, index + 1, before, after, existingTables, plan.steps.slice(0, index), plan.steps.slice(index + 1)));
|
|
1127
|
+
});
|
|
1128
|
+
return findings;
|
|
1129
|
+
}
|
|
1130
|
+
function lintCatalog(source) {
|
|
1131
|
+
const findings = [];
|
|
1132
|
+
for (const object of source.objects) {
|
|
1133
|
+
if (object.kind !== "column")
|
|
1134
|
+
continue;
|
|
1135
|
+
for (const rule of COLUMN_RULES) {
|
|
1136
|
+
const reason = rule.check(object);
|
|
1137
|
+
if (reason === undefined)
|
|
1138
|
+
continue;
|
|
1139
|
+
findings.push(finding(rule.severity, rule.code, columnPlace(object), reason));
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return findings;
|
|
1143
|
+
}
|
|
1144
|
+
function lintMigrationDirectory(directory, pending) {
|
|
1145
|
+
if (!existsSync(directory))
|
|
1146
|
+
return [];
|
|
1147
|
+
const names = readdirSync(directory).filter((file) => file.endsWith(".sql")).sort();
|
|
1148
|
+
let before = catalog([]);
|
|
1149
|
+
const findings = [];
|
|
1150
|
+
for (const file of names) {
|
|
1151
|
+
const id = file.slice(0, -".sql".length);
|
|
1152
|
+
const plan = parsePlan(readFileSync(join(directory, file), "utf8"));
|
|
1153
|
+
const after = parseCatalog2(readFileSync(join(directory, `${id}.catalog.json`), "utf8"));
|
|
1154
|
+
if (pending === undefined || pending.has(id)) {
|
|
1155
|
+
for (const item of lintPlan(plan, before, after)) {
|
|
1156
|
+
findings.push({ ...item, place: `${id} ${item.place}` });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
before = after;
|
|
1160
|
+
}
|
|
1161
|
+
return findings;
|
|
1162
|
+
}
|
|
1163
|
+
function formatFindings(findings) {
|
|
1164
|
+
if (findings.length === 0)
|
|
1165
|
+
return "";
|
|
1166
|
+
return `${findings.map(formatFinding).join(`
|
|
1167
|
+
`)}
|
|
1168
|
+
`;
|
|
1169
|
+
}
|
|
1170
|
+
function hasError(findings) {
|
|
1171
|
+
return findings.some((item) => item.severity === "error");
|
|
1172
|
+
}
|
|
1173
|
+
function lintRefusal(findings) {
|
|
1174
|
+
const doc = errorDoc("OKM1510");
|
|
1175
|
+
return new OkmError2("OKM1510", formatFindings(findings).trim(), {
|
|
1176
|
+
fix: { summary: doc?.fix ?? "Allow the finding with a reason, or change the statement." }
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
function lintStep(step, number, before, after, existingTables, earlier, later) {
|
|
1180
|
+
const hits = [];
|
|
1181
|
+
for (const rule of STEP_RULES) {
|
|
1182
|
+
for (const reason of rule.check({ step, before, after, existingTables, earlier, later })) {
|
|
1183
|
+
hits.push({ code: rule.code, severity: rule.severity, reason });
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
const triggered = new Set(hits.map((item) => item.code));
|
|
1187
|
+
const silenced = new Set;
|
|
1188
|
+
const findings = [];
|
|
1189
|
+
const place = `step ${String(number)}`;
|
|
1190
|
+
for (const allow of step.allows ?? []) {
|
|
1191
|
+
if (allow.reason.trim().length === 0 || allow.code.length === 0) {
|
|
1192
|
+
findings.push(finding("error", "OKM1510", place, "override needs a reason"));
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (!triggered.has(allow.code)) {
|
|
1196
|
+
findings.push(finding("error", "OKM1510", place, `override ${allow.code} does not match this statement`));
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
silenced.add(allow.code);
|
|
1200
|
+
}
|
|
1201
|
+
for (const hit of hits) {
|
|
1202
|
+
if (silenced.has(hit.code))
|
|
1203
|
+
continue;
|
|
1204
|
+
findings.push(finding(hit.severity, hit.code, place, hit.reason));
|
|
1205
|
+
}
|
|
1206
|
+
return findings;
|
|
1207
|
+
}
|
|
1208
|
+
function finding(severity, code, place, reason) {
|
|
1209
|
+
const doc = errorDoc(code);
|
|
1210
|
+
if (doc === undefined)
|
|
1211
|
+
throw new Error(`${code} missing from the error registry.`);
|
|
1212
|
+
return { severity, code, place, reason, fix: doc.fix };
|
|
1213
|
+
}
|
|
1214
|
+
function formatFinding(item) {
|
|
1215
|
+
return `${item.severity} ${item.code} ${item.place}: ${item.reason} -- fix: ${item.fix}`;
|
|
1216
|
+
}
|
|
1217
|
+
function tableNames(source) {
|
|
1218
|
+
const names = new Set;
|
|
1219
|
+
for (const object of source.objects) {
|
|
1220
|
+
if (object.kind === "table")
|
|
1221
|
+
names.add(object.identity.name);
|
|
1222
|
+
}
|
|
1223
|
+
return names;
|
|
1224
|
+
}
|
|
1225
|
+
function columnPlace(column) {
|
|
1226
|
+
return `${column.identity.parent.name}.${column.identity.name}`;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
497
1229
|
// src/tooling/migrate/policy.ts
|
|
498
1230
|
var READ = new Set([
|
|
499
1231
|
"plan",
|
|
@@ -637,8 +1369,8 @@ function endpointOf(url) {
|
|
|
637
1369
|
}
|
|
638
1370
|
|
|
639
1371
|
// src/tooling/migrate/project.ts
|
|
640
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
641
|
-
import { basename, join } from "node:path";
|
|
1372
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
1373
|
+
import { basename, join as join2 } from "node:path";
|
|
642
1374
|
import { pathToFileURL } from "node:url";
|
|
643
1375
|
|
|
644
1376
|
// src/dialects/pg/declarations.ts
|
|
@@ -978,6 +1710,181 @@ function objectIdentity(object) {
|
|
|
978
1710
|
return object.identity;
|
|
979
1711
|
}
|
|
980
1712
|
|
|
1713
|
+
// src/tooling/migrate/estimate.ts
|
|
1714
|
+
var LARGE_TABLE_ROWS = 1e6;
|
|
1715
|
+
var ESTIMATE_SQL = `
|
|
1716
|
+
select c.relname, c.reltuples::float8::text
|
|
1717
|
+
from pg_class c
|
|
1718
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
1719
|
+
where n.nspname = $1
|
|
1720
|
+
and c.relname = any(string_to_array($2, E'\\x1f'))
|
|
1721
|
+
and c.relkind in ('r', 'p', 'f', 'm')
|
|
1722
|
+
`;
|
|
1723
|
+
async function readRowEstimates(url, schema, tables) {
|
|
1724
|
+
const pool = open2({ url, max: 1 });
|
|
1725
|
+
try {
|
|
1726
|
+
const connection = await pool.reserve?.();
|
|
1727
|
+
if (connection === undefined) {
|
|
1728
|
+
throw new Error("The target connection cannot start a transaction.");
|
|
1729
|
+
}
|
|
1730
|
+
let began = false;
|
|
1731
|
+
try {
|
|
1732
|
+
await connection.execute("begin read only");
|
|
1733
|
+
began = true;
|
|
1734
|
+
await connection.execute("set local statement_timeout = '5s'");
|
|
1735
|
+
const map = new Map;
|
|
1736
|
+
const unique = [...new Set(tables)];
|
|
1737
|
+
if (unique.length > 0) {
|
|
1738
|
+
const result = await connection.execute(ESTIMATE_SQL, [schema, unique.join("\x1F")]);
|
|
1739
|
+
for (const row of result.rows) {
|
|
1740
|
+
const name = row[0];
|
|
1741
|
+
if (name === null || name === undefined)
|
|
1742
|
+
continue;
|
|
1743
|
+
map.set(name, classify(row[1] ?? null));
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
await connection.execute("commit");
|
|
1747
|
+
began = false;
|
|
1748
|
+
return map;
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
if (began)
|
|
1751
|
+
await connection.execute("rollback").catch(() => {
|
|
1752
|
+
return;
|
|
1753
|
+
});
|
|
1754
|
+
throw error;
|
|
1755
|
+
} finally {
|
|
1756
|
+
await connection.release();
|
|
1757
|
+
}
|
|
1758
|
+
} finally {
|
|
1759
|
+
await pool.close();
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
function annotateLock(step, estimates) {
|
|
1763
|
+
const pieces = lockPieces(step);
|
|
1764
|
+
const text = withBatches(step, pieces.map((piece) => {
|
|
1765
|
+
if (piece.table === undefined)
|
|
1766
|
+
return piece.mode;
|
|
1767
|
+
return `${piece.mode} on ${piece.table}, ${estimatePhrase(estimates.get(piece.table))}`;
|
|
1768
|
+
}).join("; "), estimates);
|
|
1769
|
+
if (step.safeRewrite === true)
|
|
1770
|
+
return `${text}; safe rewrite applied`;
|
|
1771
|
+
if (exclusiveOverLarge(pieces, estimates)) {
|
|
1772
|
+
return `${text}; note: more than ${String(LARGE_TABLE_ROWS)} estimated rows`;
|
|
1773
|
+
}
|
|
1774
|
+
return text;
|
|
1775
|
+
}
|
|
1776
|
+
function aboutRows(count) {
|
|
1777
|
+
const rounded = Math.max(0, Math.round(count));
|
|
1778
|
+
if (rounded < 1000)
|
|
1779
|
+
return `about ${String(rounded)} rows`;
|
|
1780
|
+
const units = [
|
|
1781
|
+
[1e9, "B"],
|
|
1782
|
+
[1e6, "M"],
|
|
1783
|
+
[1000, "K"]
|
|
1784
|
+
];
|
|
1785
|
+
for (const [unit, suffix] of units) {
|
|
1786
|
+
if (rounded < unit)
|
|
1787
|
+
continue;
|
|
1788
|
+
const scaled = rounded / unit;
|
|
1789
|
+
const digits = scaled >= 10 || Number.isInteger(scaled) ? 0 : 1;
|
|
1790
|
+
const text = trimZero(scaled.toFixed(digits));
|
|
1791
|
+
return `about ${text}${suffix} rows`;
|
|
1792
|
+
}
|
|
1793
|
+
return `about ${String(rounded)} rows`;
|
|
1794
|
+
}
|
|
1795
|
+
function withBatches(step, text, estimates) {
|
|
1796
|
+
const batch = step.backfill?.batch;
|
|
1797
|
+
if (batch === undefined)
|
|
1798
|
+
return text;
|
|
1799
|
+
const name = step.tables?.[0];
|
|
1800
|
+
const estimate = name === undefined ? undefined : estimates.get(name);
|
|
1801
|
+
if (estimate === undefined || estimate.kind !== "rows")
|
|
1802
|
+
return text;
|
|
1803
|
+
return `${text}, about ${String(aboutBatchCount(estimate.reltuples, batch))} batches`;
|
|
1804
|
+
}
|
|
1805
|
+
function classify(raw) {
|
|
1806
|
+
if (raw === null)
|
|
1807
|
+
return { kind: "unknown" };
|
|
1808
|
+
const value = Number(raw);
|
|
1809
|
+
if (!Number.isFinite(value) || value < 0)
|
|
1810
|
+
return { kind: "unknown" };
|
|
1811
|
+
return { kind: "rows", reltuples: value };
|
|
1812
|
+
}
|
|
1813
|
+
function estimatePhrase(estimate) {
|
|
1814
|
+
if (estimate === undefined)
|
|
1815
|
+
return "new table";
|
|
1816
|
+
if (estimate.kind === "unknown")
|
|
1817
|
+
return "rows unknown (table not analyzed)";
|
|
1818
|
+
return aboutRows(estimate.reltuples);
|
|
1819
|
+
}
|
|
1820
|
+
function lockPieces(step) {
|
|
1821
|
+
const segments = step.lock.split("; ").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
1822
|
+
const named = new Map;
|
|
1823
|
+
for (const segment of segments) {
|
|
1824
|
+
const table = lockTable(segment);
|
|
1825
|
+
if (table !== undefined)
|
|
1826
|
+
named.set(table, stripLockTable(segment));
|
|
1827
|
+
}
|
|
1828
|
+
const primary = segments[0] ?? step.lock;
|
|
1829
|
+
const primaryTable = lockTable(primary);
|
|
1830
|
+
const primaryMode = primaryTable === undefined ? primary : stripLockTable(primary);
|
|
1831
|
+
const pieces = [];
|
|
1832
|
+
const seen = new Set;
|
|
1833
|
+
if (primaryTable === undefined) {
|
|
1834
|
+
const lead = (step.tables ?? []).filter((name) => !named.has(name));
|
|
1835
|
+
if (lead.length === 0 && named.size === 0)
|
|
1836
|
+
pieces.push({ mode: step.lock, table: undefined });
|
|
1837
|
+
for (const table of lead) {
|
|
1838
|
+
seen.add(table);
|
|
1839
|
+
pieces.push({ mode: primaryMode, table });
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
for (const segment of segments) {
|
|
1843
|
+
const table = lockTable(segment);
|
|
1844
|
+
if (table === undefined) {
|
|
1845
|
+
if (primaryTable !== undefined || pieces.length === 0) {
|
|
1846
|
+
pieces.push({ mode: segment, table: undefined });
|
|
1847
|
+
}
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
if (seen.has(table))
|
|
1851
|
+
continue;
|
|
1852
|
+
seen.add(table);
|
|
1853
|
+
pieces.push({ mode: stripLockTable(segment), table });
|
|
1854
|
+
}
|
|
1855
|
+
for (const table of step.tables ?? []) {
|
|
1856
|
+
if (seen.has(table))
|
|
1857
|
+
continue;
|
|
1858
|
+
pieces.push({ mode: primaryMode, table });
|
|
1859
|
+
}
|
|
1860
|
+
return pieces;
|
|
1861
|
+
}
|
|
1862
|
+
function exclusiveOverLarge(pieces, estimates) {
|
|
1863
|
+
for (const piece of pieces) {
|
|
1864
|
+
if (piece.mode !== "ACCESS EXCLUSIVE" || piece.table === undefined)
|
|
1865
|
+
continue;
|
|
1866
|
+
const estimate = estimates.get(piece.table);
|
|
1867
|
+
if (estimate === undefined || estimate.kind !== "rows")
|
|
1868
|
+
continue;
|
|
1869
|
+
if (estimate.reltuples > LARGE_TABLE_ROWS)
|
|
1870
|
+
return true;
|
|
1871
|
+
}
|
|
1872
|
+
return false;
|
|
1873
|
+
}
|
|
1874
|
+
function lockTable(segment) {
|
|
1875
|
+
const match = /\bon\s+"((?:[^"]|"")*)"\."((?:[^"]|"")*)"/.exec(segment);
|
|
1876
|
+
const name = match?.[2];
|
|
1877
|
+
if (name === undefined)
|
|
1878
|
+
return;
|
|
1879
|
+
return name.replaceAll('""', '"');
|
|
1880
|
+
}
|
|
1881
|
+
function stripLockTable(segment) {
|
|
1882
|
+
return segment.replace(/\s+on\s+"((?:[^"]|"")*)"\."((?:[^"]|"")*)"/, "").trim();
|
|
1883
|
+
}
|
|
1884
|
+
function trimZero(text) {
|
|
1885
|
+
return text.endsWith(".0") ? text.slice(0, -2) : text;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
981
1888
|
// src/tooling/migrate/project.ts
|
|
982
1889
|
async function buildProject(cwd) {
|
|
983
1890
|
const config = await loadConfig(cwd);
|
|
@@ -989,17 +1896,21 @@ async function generateProject(cwd, name, flags) {
|
|
|
989
1896
|
writeArtifact(cwd, prepared.config, prepared.built);
|
|
990
1897
|
if (prepared.plan.steps.length === 0)
|
|
991
1898
|
return;
|
|
992
|
-
const directory =
|
|
1899
|
+
const directory = join2(cwd, prepared.config.migrations ?? "migrations");
|
|
993
1900
|
mkdirSync(directory, { recursive: true });
|
|
994
1901
|
const base = `${nextNumber(directory)}_${safeName(name)}`;
|
|
995
|
-
const sqlPath =
|
|
1902
|
+
const sqlPath = join2(directory, `${base}.sql`);
|
|
996
1903
|
writeFileSync(sqlPath, formatPlan(prepared.plan));
|
|
997
|
-
writeFileSync(
|
|
998
|
-
return sqlPath;
|
|
1904
|
+
writeFileSync(join2(directory, `${base}.catalog.json`), serializeCatalog2(prepared.built.catalog));
|
|
1905
|
+
return { path: sqlPath, findings: lintPrepared(prepared) };
|
|
999
1906
|
}
|
|
1000
|
-
async function planProject(cwd, name, flags) {
|
|
1907
|
+
async function planProject(cwd, name, flags, invoke) {
|
|
1001
1908
|
const prepared = await prepare(cwd, name, flags);
|
|
1002
|
-
return
|
|
1909
|
+
return {
|
|
1910
|
+
plan: prepared.plan,
|
|
1911
|
+
findings: lintPrepared(prepared),
|
|
1912
|
+
text: await displayPlan(prepared.config, prepared.plan, invoke)
|
|
1913
|
+
};
|
|
1003
1914
|
}
|
|
1004
1915
|
async function checkProject(cwd, flags) {
|
|
1005
1916
|
const opened = await openProject(cwd);
|
|
@@ -1013,9 +1924,9 @@ async function checkProject(cwd, flags) {
|
|
|
1013
1924
|
const targets = listTargets(opened.config);
|
|
1014
1925
|
assertTargetAlias(targets);
|
|
1015
1926
|
if (opened.config.tables !== undefined) {
|
|
1016
|
-
const directory =
|
|
1017
|
-
const files =
|
|
1018
|
-
const source =
|
|
1927
|
+
const directory = join2(cwd, opened.config.tables);
|
|
1928
|
+
const files = existsSync2(directory) ? readdirSync2(directory) : [];
|
|
1929
|
+
const source = readFileSync2(join2(cwd, opened.config.schema), "utf8");
|
|
1019
1930
|
const missing = unlistedTableFiles(source, files);
|
|
1020
1931
|
if (missing.length > 0) {
|
|
1021
1932
|
throw new OkmError2("OKM1024", `Table file not in schema: ${missing.join(", ")}.`, {
|
|
@@ -1023,14 +1934,20 @@ async function checkProject(cwd, flags) {
|
|
|
1023
1934
|
});
|
|
1024
1935
|
}
|
|
1025
1936
|
}
|
|
1937
|
+
const findings = lintOpened(opened);
|
|
1938
|
+
if (hasError(findings))
|
|
1939
|
+
throw lintRefusal(findings);
|
|
1026
1940
|
const named = flags?.target;
|
|
1027
1941
|
if (targets.length === 0)
|
|
1028
|
-
return;
|
|
1942
|
+
return findings;
|
|
1029
1943
|
if (targets.length > 1 && named === undefined)
|
|
1030
|
-
return;
|
|
1944
|
+
return findings;
|
|
1031
1945
|
const target = selectTarget(opened.config, named);
|
|
1032
|
-
|
|
1946
|
+
assertTargetPolicy(target, "check", flags?.allowProtected ?? false);
|
|
1947
|
+
assertTargetPolicy(target, "drift", flags?.allowProtected ?? false);
|
|
1948
|
+
const { assertAuthorDrift } = await import("./hqjsxnz7.js");
|
|
1033
1949
|
await assertAuthorDrift(target.url, opened.built.catalog, opened.config.roles);
|
|
1950
|
+
return findings;
|
|
1034
1951
|
}
|
|
1035
1952
|
function unlistedTableFiles(schemaSource, fileNames) {
|
|
1036
1953
|
const missing = [];
|
|
@@ -1056,15 +1973,58 @@ async function prepare(cwd, name, flags) {
|
|
|
1056
1973
|
after: opened.built.catalog,
|
|
1057
1974
|
renames: opened.renames,
|
|
1058
1975
|
replacements: flags.map((flag) => parseReplace(flag)),
|
|
1059
|
-
name
|
|
1976
|
+
name,
|
|
1977
|
+
...batchSizeField(opened.config.backfill?.batchSize)
|
|
1978
|
+
});
|
|
1979
|
+
return {
|
|
1980
|
+
config: opened.config,
|
|
1981
|
+
built: opened.built,
|
|
1982
|
+
plan,
|
|
1983
|
+
before: opened.previous,
|
|
1984
|
+
after: opened.built.catalog
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
function lintPrepared(prepared) {
|
|
1988
|
+
return lintPlan(prepared.plan, prepared.before, prepared.after);
|
|
1989
|
+
}
|
|
1990
|
+
async function displayPlan(config, plan, invoke) {
|
|
1991
|
+
const offline = formatPlan(plan);
|
|
1992
|
+
const targets = listTargets(config);
|
|
1993
|
+
if (targets.length === 0)
|
|
1994
|
+
return offline;
|
|
1995
|
+
assertTargetAlias(targets);
|
|
1996
|
+
const named = invoke?.target;
|
|
1997
|
+
if (named === undefined && targets.length !== 1)
|
|
1998
|
+
return offline;
|
|
1999
|
+
const target = selectTarget(config, named);
|
|
2000
|
+
assertTargetPolicy(target, "plan", invoke?.allowProtected ?? false);
|
|
2001
|
+
let estimates;
|
|
2002
|
+
try {
|
|
2003
|
+
const names = plan.steps.flatMap((step) => step.tables ?? []);
|
|
2004
|
+
estimates = await readRowEstimates(target.url, "public", names);
|
|
2005
|
+
} catch {
|
|
2006
|
+
return offline;
|
|
2007
|
+
}
|
|
2008
|
+
return formatPlan(plan, (step) => annotateLock(step, estimates));
|
|
2009
|
+
}
|
|
2010
|
+
function lintOpened(opened) {
|
|
2011
|
+
const plan = planMigration({
|
|
2012
|
+
before: opened.previous,
|
|
2013
|
+
after: opened.built.catalog,
|
|
2014
|
+
renames: opened.renames,
|
|
2015
|
+
name: "check",
|
|
2016
|
+
...batchSizeField(opened.config.backfill?.batchSize)
|
|
1060
2017
|
});
|
|
1061
|
-
return
|
|
2018
|
+
return [
|
|
2019
|
+
...lintPlan(plan, opened.previous, opened.built.catalog),
|
|
2020
|
+
...lintCatalog(opened.built.catalog)
|
|
2021
|
+
];
|
|
1062
2022
|
}
|
|
1063
2023
|
async function openProject(cwd) {
|
|
1064
2024
|
const config = await loadConfig(cwd);
|
|
1065
2025
|
const built = withRoles(await loadSchema(cwd, config.schema), config);
|
|
1066
2026
|
const declarations = schemaDeclarations(built);
|
|
1067
|
-
const previous = readPrevious(
|
|
2027
|
+
const previous = readPrevious(join2(cwd, config.migrations ?? "migrations"));
|
|
1068
2028
|
const stale = staleRenames(previous?.catalog, declarations.renames);
|
|
1069
2029
|
if (stale.length > 0) {
|
|
1070
2030
|
throw new OkmError2("OKM1020", stale.join(" "), {
|
|
@@ -1084,17 +2044,20 @@ function withRoles(built, config) {
|
|
|
1084
2044
|
return { ...built, catalog: attachRoles(built.catalog, config.roles) };
|
|
1085
2045
|
}
|
|
1086
2046
|
function writeArtifact(cwd, config, built) {
|
|
1087
|
-
const directory =
|
|
2047
|
+
const directory = join2(cwd, config.out ?? ".okm");
|
|
1088
2048
|
mkdirSync(directory, { recursive: true });
|
|
1089
2049
|
const text = serializeCatalog2(built.catalog);
|
|
1090
|
-
writeFileSync(
|
|
1091
|
-
writeFileSync(
|
|
2050
|
+
writeFileSync(join2(directory, "catalog.json"), text);
|
|
2051
|
+
writeFileSync(join2(directory, "catalog.hash"), `${catalogHash2(built.catalog)}
|
|
1092
2052
|
`);
|
|
1093
|
-
writeFileSync(
|
|
2053
|
+
writeFileSync(join2(directory, "types.d.ts"), `${emitRowTypes(built)}${referenceAugmentation(built)}`);
|
|
1094
2054
|
return directory;
|
|
1095
2055
|
}
|
|
2056
|
+
async function loadBuiltSchema(cwd, config) {
|
|
2057
|
+
return withRoles(await loadSchema(cwd, config.schema), config);
|
|
2058
|
+
}
|
|
1096
2059
|
async function loadConfig(cwd) {
|
|
1097
|
-
const path =
|
|
2060
|
+
const path = join2(cwd, "okmodel.config.ts");
|
|
1098
2061
|
const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
|
|
1099
2062
|
const record = isRecord(imported) ? imported : {};
|
|
1100
2063
|
const config = record.default ?? record.config;
|
|
@@ -1104,7 +2067,7 @@ async function loadConfig(cwd) {
|
|
|
1104
2067
|
return config;
|
|
1105
2068
|
}
|
|
1106
2069
|
async function loadSchema(cwd, spec) {
|
|
1107
|
-
const path =
|
|
2070
|
+
const path = join2(cwd, spec);
|
|
1108
2071
|
const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
|
|
1109
2072
|
const record = isRecord(imported) ? imported : {};
|
|
1110
2073
|
for (const value of Object.values(record)) {
|
|
@@ -1116,13 +2079,13 @@ async function loadSchema(cwd, spec) {
|
|
|
1116
2079
|
throw new OkmError2("invalid", `${spec} must export a schema().`);
|
|
1117
2080
|
}
|
|
1118
2081
|
function readPrevious(directory) {
|
|
1119
|
-
if (!
|
|
2082
|
+
if (!existsSync2(directory))
|
|
1120
2083
|
return;
|
|
1121
|
-
const catalogs =
|
|
2084
|
+
const catalogs = readdirSync2(directory).filter((file) => file.endsWith(".catalog.json")).sort();
|
|
1122
2085
|
const latest = catalogs.at(-1);
|
|
1123
2086
|
if (latest === undefined)
|
|
1124
2087
|
return;
|
|
1125
|
-
const text =
|
|
2088
|
+
const text = readFileSync2(join2(directory, latest), "utf8");
|
|
1126
2089
|
return { catalog: parseCatalog2(text) };
|
|
1127
2090
|
}
|
|
1128
2091
|
function referenceAugmentation(source) {
|
|
@@ -1147,8 +2110,8 @@ function referenceAugmentation(source) {
|
|
|
1147
2110
|
}
|
|
1148
2111
|
function nextNumber(directory) {
|
|
1149
2112
|
let max = 0;
|
|
1150
|
-
if (
|
|
1151
|
-
for (const file of
|
|
2113
|
+
if (existsSync2(directory)) {
|
|
2114
|
+
for (const file of readdirSync2(directory)) {
|
|
1152
2115
|
const match = /^(\d+)_/.exec(file);
|
|
1153
2116
|
const value = match?.[1];
|
|
1154
2117
|
if (value !== undefined)
|
|
@@ -1189,14 +2152,14 @@ function projectImportsValidate(cwd) {
|
|
|
1189
2152
|
function directoryImportsValidate(dir) {
|
|
1190
2153
|
let entries;
|
|
1191
2154
|
try {
|
|
1192
|
-
entries =
|
|
2155
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
1193
2156
|
} catch {
|
|
1194
2157
|
return false;
|
|
1195
2158
|
}
|
|
1196
2159
|
for (const entry of entries) {
|
|
1197
2160
|
if (entry.name.startsWith(".") || SKIP_SCAN.has(entry.name))
|
|
1198
2161
|
continue;
|
|
1199
|
-
const path =
|
|
2162
|
+
const path = join2(dir, entry.name);
|
|
1200
2163
|
if (entry.isDirectory()) {
|
|
1201
2164
|
if (directoryImportsValidate(path))
|
|
1202
2165
|
return true;
|
|
@@ -1204,7 +2167,7 @@ function directoryImportsValidate(dir) {
|
|
|
1204
2167
|
}
|
|
1205
2168
|
if (!/\.(?:ts|tsx|mts|cts)$/.test(entry.name))
|
|
1206
2169
|
continue;
|
|
1207
|
-
if (runtimeImportsValidate(
|
|
2170
|
+
if (runtimeImportsValidate(readFileSync2(path, "utf8")))
|
|
1208
2171
|
return true;
|
|
1209
2172
|
}
|
|
1210
2173
|
return false;
|
|
@@ -1226,4 +2189,4 @@ function runtimeImportsValidate(source) {
|
|
|
1226
2189
|
return false;
|
|
1227
2190
|
}
|
|
1228
2191
|
|
|
1229
|
-
export { errorDoc, assertTargetPolicy, assertDirectConnection, listTargets, selectTarget, buildProject, generateProject, planProject, checkProject, projectHead, openProject, loadConfig };
|
|
2192
|
+
export { errorDoc, lintMigrationDirectory, formatFindings, hasError, lintRefusal, assertTargetPolicy, assertDirectConnection, listTargets, selectTarget, buildProject, generateProject, planProject, checkProject, projectHead, openProject, loadBuiltSchema, loadConfig, loadSchema };
|