@intentius/chant-lexicon-sql 0.112.1 → 0.114.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.
Files changed (70) hide show
  1. package/dist/clickhouse/apply/apply.d.ts +6 -1
  2. package/dist/clickhouse/apply/apply.d.ts.map +1 -1
  3. package/dist/clickhouse/apply/statements.d.ts +85 -1
  4. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  5. package/dist/clickhouse/live/bind.d.ts +11 -1
  6. package/dist/clickhouse/live/bind.d.ts.map +1 -1
  7. package/dist/clickhouse/plan/commands.d.ts +12 -1
  8. package/dist/clickhouse/plan/commands.d.ts.map +1 -1
  9. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  10. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  11. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  12. package/dist/clickhouse/plan/schema.d.ts +4 -3
  13. package/dist/clickhouse/plan/schema.d.ts.map +1 -1
  14. package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
  15. package/dist/clickhouse/rebuild/receipts.d.ts +5 -0
  16. package/dist/clickhouse/rebuild/receipts.d.ts.map +1 -1
  17. package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
  18. package/dist/clickhouse/topology.d.ts +112 -0
  19. package/dist/clickhouse/topology.d.ts.map +1 -0
  20. package/dist/clickhouse.d.ts +1 -0
  21. package/dist/clickhouse.d.ts.map +1 -1
  22. package/dist/codegen/classifier-page.d.ts.map +1 -1
  23. package/dist/codegen/postgres-classifier-page.d.ts.map +1 -1
  24. package/dist/config.d.ts +46 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/core/statements.d.ts +106 -0
  27. package/dist/core/statements.d.ts.map +1 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/integrity.json +2 -2
  31. package/dist/manifest.json +1 -1
  32. package/dist/migration-statements.d.ts +63 -0
  33. package/dist/migration-statements.d.ts.map +1 -0
  34. package/dist/postgres/apply/apply.d.ts.map +1 -1
  35. package/dist/postgres/apply/statements.d.ts +91 -1
  36. package/dist/postgres/apply/statements.d.ts.map +1 -1
  37. package/package.json +2 -2
  38. package/src/clickhouse/apply/apply.ts +35 -56
  39. package/src/clickhouse/apply/statements.ts +168 -31
  40. package/src/clickhouse/live/bind.ts +13 -1
  41. package/src/clickhouse/live/live.test.ts +10 -0
  42. package/src/clickhouse/plan/commands.ts +63 -9
  43. package/src/clickhouse/plan/deep.ts +2 -1
  44. package/src/clickhouse/plan/diff.ts +3 -1
  45. package/src/clickhouse/plan/normalize.ts +11 -2
  46. package/src/clickhouse/plan/schema.ts +6 -5
  47. package/src/clickhouse/rebuild/backfill.ts +1 -0
  48. package/src/clickhouse/rebuild/receipts.ts +14 -4
  49. package/src/clickhouse/rebuild/steps.ts +8 -5
  50. package/src/clickhouse/topology-replicated.e2e.test.ts +125 -0
  51. package/src/clickhouse/topology.e2e.test.ts +183 -0
  52. package/src/clickhouse/topology.test.ts +309 -0
  53. package/src/clickhouse/topology.ts +477 -0
  54. package/src/clickhouse.ts +16 -0
  55. package/src/codegen/classifier-page.ts +19 -0
  56. package/src/codegen/postgres-classifier-page.ts +17 -0
  57. package/src/config.ts +33 -0
  58. package/src/core/statements.ts +129 -0
  59. package/src/index.ts +15 -0
  60. package/src/migration-statements.test.ts +384 -0
  61. package/src/migration-statements.ts +262 -0
  62. package/src/op/__snapshots__/watch-pipeline.test.ts.snap +93 -0
  63. package/src/op/activities/clickhouse-apply.ts +1 -1
  64. package/src/op/activities/clickhouse-rebuild.ts +1 -1
  65. package/src/op/apply-gate-create.e2e.test.ts +280 -0
  66. package/src/op/watch-drift.e2e.test.ts +302 -0
  67. package/src/op/watch-pipeline.test.ts +113 -0
  68. package/src/plugin.test.ts +9 -0
  69. package/src/postgres/apply/apply.ts +38 -96
  70. package/src/postgres/apply/statements.ts +145 -2
@@ -26,11 +26,11 @@ import { isTrivia, tokenizeText, type Token } from "../tokens";
26
26
  import { parseStatements, type CommentNode } from "../parser";
27
27
  import { quoteIdent } from "../keywords";
28
28
  import { POSTGRES_ENTITY_TYPES, type PostgresEntityType } from "../entity-types";
29
- import { addConstraintRule } from "../plan/diff";
29
+ import { addConstraintRule, matchPgObjects } from "../plan/diff";
30
30
  import type { PgChange } from "../plan/diff";
31
31
  import { PG_CLASSIFIER_RULES, type PgChangeClass, type PgClassifierRuleId } from "../plan/rules";
32
32
  import { canonicalOptions, sameConstraint, type CanonicalConstraint, type CanonicalKind } from "../plan/normalize";
33
- import type { PgDiffObject } from "../plan/schema";
33
+ import type { PgDiffObject, PgSchemaObject } from "../plan/schema";
34
34
  import { migrationTarget } from "../migrate/handoff";
35
35
  import type { CheckDef, ColumnDef, DomainProps, ExclusionDef, ForeignKeyDef, KeyDef, SequenceProps, TableProps } from "../entities";
36
36
 
@@ -601,3 +601,146 @@ export function refusalDetail(changes: readonly PgChange[], name: string, declar
601
601
  : "No migration Op makes this change yet: make it by hand as expand and contract (add the new, write both, backfill, move readers, then drop the old).";
602
602
  return `${name} needs expand and contract, which no in-place statement makes, so nothing was sent for it. ${lines.join(" ")} ${instead}`;
603
603
  }
604
+
605
+ // ── A schema's statements ──────────────────────────────────────────────
606
+
607
+ /**
608
+ * What one declared object takes, as the applier and the offline renderer
609
+ * (`../../migration-statements.ts`) both see it:
610
+ *
611
+ * - `create` or `alter`: the statements, in the order they run. An `alter`
612
+ * with none is an object already as declared.
613
+ * - `foreign`: another tool keeps the object; it is not chant's to change.
614
+ * - `refused`: a change only expand and contract makes; nothing is sent for
615
+ * the object, and `detail` names the migration Op where one makes it.
616
+ * - `unsupported`: a change with no in-place statement; nothing is sent.
617
+ * - `withheld`: a column drop, which destroys data, in a plan not allowed to
618
+ * delete.
619
+ */
620
+ export type PgObjectStatements =
621
+ | { verdict: "create" | "alter"; obj: DeclaredPgObject; changes: PgChange[]; steps: PgStep[]; live?: PgDiffObject }
622
+ | { verdict: "foreign"; obj: DeclaredPgObject; changes: PgChange[]; live: PgDiffObject; detail: string }
623
+ | { verdict: "refused"; obj: DeclaredPgObject; changes: PgChange[]; refused: PgChange[]; live?: PgDiffObject; detail: string }
624
+ | { verdict: "unsupported"; obj: DeclaredPgObject; changes: PgChange[]; unsupported: UnsupportedChange[]; live: PgDiffObject; detail: string }
625
+ | { verdict: "withheld"; obj: DeclaredPgObject; changes: PgChange[]; withheld: PgChange[]; live?: PgDiffObject; detail: string };
626
+
627
+ /** An object the declarations no longer hold, and the `DROP` for it. */
628
+ export interface PgDropStatement {
629
+ /** The key the changes name it by. */
630
+ key: string;
631
+ kind: CanonicalKind;
632
+ schema?: string;
633
+ name: string;
634
+ step: PgStep;
635
+ }
636
+
637
+ export interface PgStatementPlanInput {
638
+ /** The declared objects in creation order, in the canonical form the changes were found for. */
639
+ declared: readonly DeclaredPgObject[];
640
+ /** The classified changes from the current schema to the declared one. */
641
+ changes: readonly PgChange[];
642
+ /** The current schema, keyed as the changes key objects. */
643
+ current: readonly PgSchemaObject[];
644
+ /** How the changes key a declared object: its qualified key against a server (the default), its export name between two builds. */
645
+ keyOf?: (obj: DeclaredPgObject) => string;
646
+ /** The major the statements are for. */
647
+ major: number;
648
+ /** The marker every created object and restamped comment carries. */
649
+ marker?: OwnershipMarker;
650
+ /** Column drops are planned, not withheld. Default: off. */
651
+ allowDestructive?: boolean;
652
+ /** Whether the current object carries the marker already; one that does not is restamped. Default: it does. */
653
+ carriesMarker?: (live: PgDiffObject, currentKey: string) => boolean;
654
+ /** The comment an extension's control file sets, kept under the trailer when the declaration sets none. */
655
+ extensionComment?: (name: string) => string | undefined;
656
+ }
657
+
658
+ export interface PgStatementPlan {
659
+ /** One entry per declared object, in creation order. */
660
+ objects: PgObjectStatements[];
661
+ /** The objects the declarations no longer hold, in drop order: views first, a schema last. */
662
+ drops: PgDropStatement[];
663
+ }
664
+
665
+ /** Drop order: what reads from a table before the table, types after the tables using them, a schema last. */
666
+ const DROP_ORDER: Record<CanonicalKind, number> = { materializedView: 0, view: 1, index: 2, table: 3, sequence: 4, domain: 5, enum: 6, extension: 7, schema: 8 };
667
+
668
+ /**
669
+ * The statements that take the current schema to the declared one, from the
670
+ * classified changes between them. Pure: nothing is read or sent. The applier
671
+ * (`./apply.ts`) groups the steps into transactions and runs them;
672
+ * `diffStatements` (`../../migration-statements.ts`) renders the same steps
673
+ * for a migration file.
674
+ */
675
+ export function planPgStatements(input: PgStatementPlanInput): PgStatementPlan {
676
+ const keyOf = input.keyOf ?? ((o: DeclaredPgObject) => o.key);
677
+ const byObject = new Map<string, PgChange[]>();
678
+ for (const c of input.changes) byObject.set(c.object, [...(byObject.get(c.object) ?? []), c]);
679
+ const currentByKey = new Map(input.current.map((o) => [o.key, o.canonical]));
680
+ const matched = new Map<string, string>();
681
+ for (const m of matchPgObjects(input.current, input.declared.map((o) => ({ key: keyOf(o), canonical: o.canonical })))) {
682
+ if (m.kind === "matched") matched.set(m.after.key, m.before.key);
683
+ }
684
+
685
+ const recreatedRelations = new Set<string>();
686
+ const objects = input.declared.map((obj): PgObjectStatements => {
687
+ const key = keyOf(obj);
688
+ const currentKey = matched.get(key);
689
+ const live = currentKey !== undefined ? currentByKey.get(currentKey) : undefined;
690
+ const mine = byObject.get(key) ?? [];
691
+ const withLive = live ? { live } : {};
692
+
693
+ if (live?.foreign) return { verdict: "foreign", obj, changes: mine, live, detail: `${live.foreign} keeps ${obj.name}; it is not chant's to change` };
694
+ const refused = mine.filter(isRefused);
695
+ if (refused.length > 0) return { verdict: "refused", obj, changes: mine, refused, ...withLive, detail: refusalDetail(refused, obj.name, obj.canonical) };
696
+ const destructive = mine.filter(isDestructive);
697
+ if (destructive.length > 0 && !input.allowDestructive) {
698
+ return {
699
+ verdict: "withheld",
700
+ obj,
701
+ changes: mine,
702
+ withheld: destructive,
703
+ ...withLive,
704
+ detail: `drops ${destructive.map((c) => c.field).join(", ")}, which destroys the data in it; an apply that may delete (prune, ApplyOp delete "owned-only" or "gated") makes it`,
705
+ };
706
+ }
707
+
708
+ // An index on a materialized view this plan recreates went with the view, so it is created again.
709
+ const onRecreated = obj.canonical.kind === "index" && recreatedRelations.has(String(obj.canonical.fields.table));
710
+ if (!live || onRecreated) {
711
+ const base = obj.canonical.kind === "extension" && !live ? input.extensionComment?.(obj.canonical.name) : undefined;
712
+ // An index on a table that exists is built under the scan timeout (SQLPG240, SQLPG241); one on a new table is part of the create.
713
+ const how = mine.find((c) => c.rule === "SQLPG200" || c.rule === "SQLPG240" || c.rule === "SQLPG241");
714
+ const steps = createSteps(obj, input.marker, { ...(base !== undefined ? { base } : {}), ...(how ? { cls: how.class, rule: how.rule } : {}) });
715
+ return { verdict: "create", obj, changes: mine, steps, ...withLive };
716
+ }
717
+
718
+ const altered = alterSteps(obj, live, mine, { ...(input.marker ? { marker: input.marker } : {}), major: input.major });
719
+ if (altered.unsupported.length > 0) {
720
+ return {
721
+ verdict: "unsupported",
722
+ obj,
723
+ changes: mine,
724
+ unsupported: altered.unsupported,
725
+ live,
726
+ detail: `${obj.name}: ${altered.unsupported.map((u) => `${u.change.rule} ${u.change.field}: ${u.why}`).join("; ")}. Nothing was sent for it.`,
727
+ };
728
+ }
729
+ const steps = altered.steps;
730
+ if (mine.some((c) => c.rule === "SQLPG252")) recreatedRelations.add(`${obj.canonical.schema}.${obj.canonical.name}`);
731
+ const stamp = commentStatement(obj, input.marker);
732
+ if (!steps.some((s) => s.sql === stamp) && !(input.carriesMarker?.(live, currentKey!) ?? true)) steps.push(step(stamp, "metadata", "SQLPG216"));
733
+ return { verdict: "alter", obj, changes: mine, steps, live };
734
+ });
735
+
736
+ const drops: PgDropStatement[] = [];
737
+ for (const c of input.changes) {
738
+ if (!((c.rule === "SQLPG270" || c.rule === "SQLPG242") && c.after === undefined)) continue;
739
+ const o = currentByKey.get(c.object);
740
+ if (!o || o.foreign) continue;
741
+ const schema = o.kind === "schema" || o.kind === "extension" ? undefined : o.schema;
742
+ drops.push({ key: c.object, kind: o.kind, ...(schema !== undefined ? { schema } : {}), name: o.name, step: dropStatement(o.kind, schema, o.name) });
743
+ }
744
+ drops.sort((a, b) => DROP_ORDER[a.kind] - DROP_ORDER[b.kind]);
745
+ return { objects, drops };
746
+ }