@intentius/chant-lexicon-sql 0.114.1 → 0.116.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 (142) hide show
  1. package/dist/clickhouse/apply/mutations.d.ts +7 -1
  2. package/dist/clickhouse/apply/mutations.d.ts.map +1 -1
  3. package/dist/clickhouse/container.d.ts +22 -7
  4. package/dist/clickhouse/container.d.ts.map +1 -1
  5. package/dist/clickhouse/plan/deep.d.ts +5 -0
  6. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  7. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  8. package/dist/clickhouse/rebuild/backfill.d.ts +11 -5
  9. package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
  10. package/dist/clickhouse/rebuild/op.d.ts +29 -0
  11. package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
  12. package/dist/clickhouse/rebuild/partitions.d.ts +8 -0
  13. package/dist/clickhouse/rebuild/partitions.d.ts.map +1 -1
  14. package/dist/clickhouse/rebuild/receipts.d.ts +4 -6
  15. package/dist/clickhouse/rebuild/receipts.d.ts.map +1 -1
  16. package/dist/clickhouse/rebuild/shards.d.ts +59 -0
  17. package/dist/clickhouse/rebuild/shards.d.ts.map +1 -0
  18. package/dist/clickhouse/rebuild/steps.d.ts +3 -0
  19. package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
  20. package/dist/clickhouse/rebuild/verify.d.ts +4 -0
  21. package/dist/clickhouse/rebuild/verify.d.ts.map +1 -1
  22. package/dist/codegen/registry.d.ts.map +1 -1
  23. package/dist/config.d.ts +14 -0
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/core/receipts.d.ts +5 -3
  26. package/dist/core/receipts.d.ts.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/integrity.json +5 -5
  30. package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
  31. package/dist/lsp/postgres-template.d.ts.map +1 -1
  32. package/dist/manifest.json +1 -1
  33. package/dist/meta.json +15 -0
  34. package/dist/okf/index.md +3 -0
  35. package/dist/okf/types/PostgresFunction.md +9 -0
  36. package/dist/okf/types/PostgresProcedure.md +9 -0
  37. package/dist/okf/types/PostgresTrigger.md +9 -0
  38. package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
  39. package/dist/op/activities/index.d.ts +13 -1
  40. package/dist/op/activities/index.d.ts.map +1 -1
  41. package/dist/op/activities/sql-exec.d.ts +41 -0
  42. package/dist/op/activities/sql-exec.d.ts.map +1 -0
  43. package/dist/op/activity-contracts.d.ts +20 -0
  44. package/dist/op/activity-contracts.d.ts.map +1 -1
  45. package/dist/plugin.d.ts.map +1 -1
  46. package/dist/postgres/apply/apply.d.ts +6 -1
  47. package/dist/postgres/apply/apply.d.ts.map +1 -1
  48. package/dist/postgres/apply/statements.d.ts +9 -2
  49. package/dist/postgres/apply/statements.d.ts.map +1 -1
  50. package/dist/postgres/entities.d.ts +110 -3
  51. package/dist/postgres/entities.d.ts.map +1 -1
  52. package/dist/postgres/entity-types.d.ts +3 -0
  53. package/dist/postgres/entity-types.d.ts.map +1 -1
  54. package/dist/postgres/import/generator.d.ts.map +1 -1
  55. package/dist/postgres/import/live-export.d.ts.map +1 -1
  56. package/dist/postgres/live/catalog.d.ts +17 -5
  57. package/dist/postgres/live/catalog.d.ts.map +1 -1
  58. package/dist/postgres/live/describe-resources.d.ts +6 -1
  59. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  60. package/dist/postgres/migrate/receipts.d.ts +8 -5
  61. package/dist/postgres/migrate/receipts.d.ts.map +1 -1
  62. package/dist/postgres/parser.d.ts +82 -5
  63. package/dist/postgres/parser.d.ts.map +1 -1
  64. package/dist/postgres/plan/deep.d.ts.map +1 -1
  65. package/dist/postgres/plan/diff.d.ts +1 -1
  66. package/dist/postgres/plan/diff.d.ts.map +1 -1
  67. package/dist/postgres/plan/normalize.d.ts +21 -2
  68. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  69. package/dist/postgres/plan/rules.d.ts +6 -0
  70. package/dist/postgres/plan/rules.d.ts.map +1 -1
  71. package/dist/postgres/plan/schema.d.ts +11 -1
  72. package/dist/postgres/plan/schema.d.ts.map +1 -1
  73. package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
  74. package/dist/postgres.d.ts +1 -1
  75. package/dist/postgres.d.ts.map +1 -1
  76. package/dist/receipts.d.ts +102 -0
  77. package/dist/receipts.d.ts.map +1 -0
  78. package/dist/rules/postgres-templates.ts +2 -2
  79. package/dist/rules/sqlpg001.ts +3 -0
  80. package/dist/validate.d.ts.map +1 -1
  81. package/package.json +2 -2
  82. package/src/clickhouse/apply/mutations.ts +9 -3
  83. package/src/clickhouse/container.ts +39 -11
  84. package/src/clickhouse/plan/cluster-drift.e2e.test.ts +117 -0
  85. package/src/clickhouse/plan/deep.ts +102 -3
  86. package/src/clickhouse/plan/normalize.ts +10 -0
  87. package/src/clickhouse/plan/plan.test.ts +10 -0
  88. package/src/clickhouse/rebuild/backfill.ts +85 -42
  89. package/src/clickhouse/rebuild/op.ts +80 -35
  90. package/src/clickhouse/rebuild/partitions.ts +11 -0
  91. package/src/clickhouse/rebuild/rebuild-sharded.e2e.test.ts +161 -0
  92. package/src/clickhouse/rebuild/rebuild.e2e.test.ts +110 -4
  93. package/src/clickhouse/rebuild/rebuild.test.ts +56 -4
  94. package/src/clickhouse/rebuild/receipts.ts +4 -6
  95. package/src/clickhouse/rebuild/shards.ts +104 -0
  96. package/src/clickhouse/rebuild/steps.ts +4 -0
  97. package/src/clickhouse/rebuild/verify.ts +14 -5
  98. package/src/codegen/postgres-classifier-page.ts +1 -1
  99. package/src/codegen/registry.ts +3 -0
  100. package/src/config.ts +7 -0
  101. package/src/core/receipts.ts +5 -3
  102. package/src/generated/lexicon-sql.json +15 -0
  103. package/src/index.ts +1 -1
  104. package/src/lint/rules/postgres-templates.ts +2 -2
  105. package/src/lint/rules/sqlpg001.ts +3 -0
  106. package/src/lsp/postgres-template.ts +6 -0
  107. package/src/mcp/postgres.ts +1 -1
  108. package/src/op/activities/clickhouse-rebuild.ts +32 -7
  109. package/src/op/activities/index.ts +21 -4
  110. package/src/op/activities/sql-exec.ts +84 -0
  111. package/src/op/activity-contracts.ts +15 -0
  112. package/src/op/effect-receipts.e2e.test.ts +277 -0
  113. package/src/plugin.ts +3 -0
  114. package/src/postgres/apply/apply.ts +21 -5
  115. package/src/postgres/apply/statements.ts +53 -11
  116. package/src/postgres/entities.ts +332 -9
  117. package/src/postgres/entity-types.ts +3 -0
  118. package/src/postgres/import/generator.ts +5 -2
  119. package/src/postgres/import/live-export.ts +4 -0
  120. package/src/postgres/live/catalog.ts +81 -5
  121. package/src/postgres/live/describe-resources.ts +17 -7
  122. package/src/postgres/live/routines.e2e.test.ts +173 -0
  123. package/src/postgres/migrate/batches.e2e.test.ts +2 -1
  124. package/src/postgres/migrate/migrate.e2e.test.ts +2 -1
  125. package/src/postgres/migrate/migrate.test.ts +3 -2
  126. package/src/postgres/migrate/receipts.ts +18 -10
  127. package/src/postgres/parser.ts +314 -8
  128. package/src/postgres/plan/commands.ts +2 -2
  129. package/src/postgres/plan/deep.ts +57 -17
  130. package/src/postgres/plan/diff.ts +46 -6
  131. package/src/postgres/plan/normalize.ts +158 -4
  132. package/src/postgres/plan/rules.ts +36 -0
  133. package/src/postgres/plan/schema.ts +25 -6
  134. package/src/postgres/plan/server-normalize.ts +24 -0
  135. package/src/postgres/routines.test.ts +150 -0
  136. package/src/postgres.ts +11 -0
  137. package/src/receipts.test.ts +91 -0
  138. package/src/receipts.ts +259 -0
  139. package/src/skill-defs.ts +1 -1
  140. package/src/skills/chant-sql-postgres.md +18 -2
  141. package/src/skills/chant-sql-rebuild.md +5 -1
  142. package/src/validate.ts +3 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Functions, procedures and triggers against a live server (#3680): applied
3
+ * as declared, they plan with no change and read back with no drift; a body,
4
+ * a result type and a trigger's condition changed in the declaration are
5
+ * planned with their rules and applied; a function replaced by hand is
6
+ * drift; and an import writes them back as `func`, `procedure` and
7
+ * `trigger` declarations that parse.
8
+ *
9
+ * The server: `CHANT_SQL_E2E_POSTGRES_URL` (a `postgres://user@host:port/db`
10
+ * URL, with `CHANT_SQL_E2E_POSTGRES_PASSWORD`) names a running one, such as
11
+ * the one `chant emulator up --lexicon sql` starts. Without it, a throwaway
12
+ * server at the pin, skipped cleanly when Docker is not available. The test
13
+ * writes only to the schema `chant_e2e_3680`, dropped afterwards.
14
+ */
15
+
16
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
20
+ import { dockerAvailable, startTestPostgres, type TestPostgres } from "../testing/server";
21
+ import { connectPostgres, type PostgresClient, type PostgresEndpoint } from "./client";
22
+ import { readLiveSchema } from "./catalog";
23
+ import { planPgAgainstServer } from "../plan/commands";
24
+ import { observeResourcesDeep } from "../plan/deep";
25
+ import { postgresApply } from "../../op/activities/postgres-apply";
26
+ import { objectsToIR } from "../import/ir";
27
+ import { PostgresGenerator } from "../import/generator";
28
+ import { liveProps } from "../plan/deep";
29
+ import { declaredObjects } from "../apply/apply";
30
+
31
+ const SCHEMA = "chant_e2e_3680";
32
+ const given = process.env.CHANT_SQL_E2E_POSTGRES_URL;
33
+ const enabled = given ? true : await dockerAvailable();
34
+ const dir = mkdtempSync(join(tmpdir(), "chant-pg-routines-"));
35
+ let server: TestPostgres | undefined;
36
+ let endpoint: PostgresEndpoint;
37
+ let admin: PostgresClient;
38
+
39
+ beforeAll(async () => {
40
+ if (!enabled) return;
41
+ if (given) endpoint = { url: given, password: process.env.CHANT_SQL_E2E_POSTGRES_PASSWORD ?? "" };
42
+ else {
43
+ server = await startTestPostgres();
44
+ endpoint = server.endpoint();
45
+ }
46
+ admin = await connectPostgres(endpoint);
47
+ await admin.query(`DROP SCHEMA IF EXISTS ${SCHEMA} CASCADE`);
48
+ }, 600_000);
49
+
50
+ afterAll(async () => {
51
+ if (enabled) {
52
+ await admin?.query(`DROP SCHEMA IF EXISTS ${SCHEMA} CASCADE`).catch(() => undefined);
53
+ await admin?.end();
54
+ }
55
+ await server?.stop();
56
+ rmSync(dir, { recursive: true, force: true });
57
+ });
58
+
59
+ interface Obj {
60
+ export: string;
61
+ type: string;
62
+ ddl: string;
63
+ dependsOn?: string[];
64
+ }
65
+
66
+ const MARKER = { stack: "e2e3680", env: "test" };
67
+ const profile = () => ({
68
+ config: { ownership: MARKER, sql: { profiles: { e2e: { url: endpoint.url, password: { env: "PG_E2E_PASSWORD" }, schemas: [SCHEMA] } } } },
69
+ env: { PG_E2E_PASSWORD: endpoint.password ?? "" },
70
+ });
71
+
72
+ function writeBuild(file: string, objects: Obj[]): string {
73
+ const path = join(dir, file);
74
+ writeFileSync(path, JSON.stringify({ dialect: "postgres", postgresMajor: 18, objects: objects.map((o) => ({ dependsOn: [], ...o })) }));
75
+ return path;
76
+ }
77
+
78
+ const apply = (path: string) => postgresApply({ buildPath: path, environment: "e2e" }, undefined, { ...profile(), log: () => undefined });
79
+ const plan = (path: string) => planPgAgainstServer("e2e", path, profile());
80
+
81
+ const S = SCHEMA;
82
+ const objects = (v: { body: string; totalReturns: string; when: string }): Obj[] => [
83
+ { export: "app", type: "Postgres::Schema", ddl: `CREATE SCHEMA ${S}` },
84
+ {
85
+ export: "orders",
86
+ type: "Postgres::Table",
87
+ dependsOn: ["app"],
88
+ ddl: `CREATE TABLE ${S}.orders (id bigint PRIMARY KEY, status text NOT NULL, amount numeric(12,2), note varchar(40), updated_at timestamptz)`,
89
+ },
90
+ {
91
+ export: "touch",
92
+ type: "Postgres::Function",
93
+ dependsOn: ["app"],
94
+ ddl: `CREATE FUNCTION ${S}.touch() RETURNS trigger LANGUAGE plpgsql SET search_path = ${S} AS $$\nBEGIN\n ${v.body}\n RETURN NEW;\nEND\n$$;\nCOMMENT ON FUNCTION ${S}.touch() IS 'Stamps updated_at'`,
95
+ },
96
+ {
97
+ export: "total",
98
+ type: "Postgres::Function",
99
+ dependsOn: ["orders"],
100
+ ddl: `CREATE FUNCTION ${S}.total(min_amount numeric DEFAULT 0, label varchar(10) = 'all') RETURNS ${v.totalReturns} LANGUAGE sql STABLE PARALLEL SAFE AS $$ SELECT coalesce(sum(amount), 0)::${v.totalReturns} FROM ${S}.orders WHERE amount >= min_amount $$`,
101
+ },
102
+ {
103
+ export: "archive",
104
+ type: "Postgres::Procedure",
105
+ dependsOn: ["orders"],
106
+ ddl: `CREATE PROCEDURE ${S}.archive(IN before_id bigint, INOUT archived int DEFAULT NULL) LANGUAGE plpgsql AS $$ BEGIN DELETE FROM ${S}.orders WHERE id < before_id; GET DIAGNOSTICS archived = ROW_COUNT; END $$`,
107
+ },
108
+ {
109
+ export: "ordersTouch",
110
+ type: "Postgres::Trigger",
111
+ dependsOn: ["orders", "touch"],
112
+ ddl: `CREATE TRIGGER orders_touch BEFORE INSERT OR UPDATE OF status, note ON ${S}.orders FOR EACH ROW WHEN (${v.when}) EXECUTE FUNCTION ${S}.touch('x', 1)`,
113
+ },
114
+ ];
115
+
116
+ const V1 = { body: "NEW.updated_at := now();", totalReturns: "numeric", when: "NEW.note IS DISTINCT FROM 'skip'" };
117
+ const V2 = { body: "NEW.updated_at := clock_timestamp();", totalReturns: "double precision", when: "NEW.note <> 'skip' AND NEW.status = 'open'" };
118
+
119
+ describe.skipIf(!enabled)("functions, procedures and triggers on a live server", () => {
120
+ test("apply, plan, drift, change and import", async () => {
121
+ const v1 = writeBuild("v1.json", objects(V1));
122
+ const first = await apply(v1);
123
+ expect(first.failed).toEqual([]);
124
+ expect(first.applied.map((a) => [a.name, a.action])).toEqual([
125
+ [S, "created"],
126
+ [`${S}.orders`, "created"],
127
+ [`${S}.touch()`, "created"],
128
+ [`${S}.total(numeric,character varying)`, "created"],
129
+ [`${S}.archive(bigint,integer)`, "created"],
130
+ [`orders_touch ON ${S}.orders`, "created"],
131
+ ]);
132
+ // The trigger fires, with its WHEN and its arguments.
133
+ await admin.query(`INSERT INTO ${S}.orders (id, status, amount) VALUES (1, 'open', 10), (2, 'open', 5)`);
134
+ expect((await admin.query<{ n: string }>(`SELECT count(*) AS n FROM ${S}.orders WHERE updated_at IS NOT NULL`))[0]!.n).toBe("2");
135
+
136
+ // As declared: no change, and no drift, the WHEN the server prints with its casts included.
137
+ expect((await plan(v1)).changes).toEqual([]);
138
+ const declared = declaredObjects(JSON.stringify({ dialect: "postgres", objects: objects(V1).map((o) => ({ dependsOn: [], ...o })) }));
139
+ const entities = new Map(declared.map((o) => [o.exportName, { entityType: o.type, props: o.props }]));
140
+ const deep = await observeResourcesDeep({ environment: "e2e", entityNames: [...entities.keys()], entities, ...profile() });
141
+ expect(deep.unobserved ?? {}).toEqual({});
142
+ const strip = (p: Record<string, unknown>) => Object.fromEntries(Object.entries(p).filter(([k]) => !["ddl", "source", "reads", "orReplace"].includes(k)));
143
+ for (const [name, e] of entities) expect(strip(deep.resources[name]!.properties), name).toEqual(strip(e.props));
144
+
145
+ // Replaced by hand: drift, in the plan and in the deep read.
146
+ await admin.query(`CREATE OR REPLACE FUNCTION ${S}.touch() RETURNS trigger LANGUAGE plpgsql SET search_path = ${S} AS $$ BEGIN RETURN NEW; END $$`);
147
+ expect((await plan(v1)).changes.map((c) => [c.object, c.field, c.rule])).toEqual([[`touch (${S}.touch())`, "body", "SQLPG280"]]);
148
+ const drifted = await observeResourcesDeep({ environment: "e2e", entityNames: ["touch"], entities, ...profile() });
149
+ expect(drifted.resources.touch!.properties.body).toBe(" BEGIN RETURN NEW; END ");
150
+
151
+ // Changed in the declaration: a body (OR REPLACE), a result type (drop and create), a trigger's condition (OR REPLACE).
152
+ const v2 = writeBuild("v2.json", objects(V2));
153
+ expect((await plan(v2)).changes.map((c) => [c.object, c.rule])).toEqual([
154
+ [`touch (${S}.touch())`, "SQLPG280"],
155
+ [`total (${S}.total(numeric,character varying))`, "SQLPG281"],
156
+ [`ordersTouch (${S}.orders_touch ON ${S}.orders)`, "SQLPG284"],
157
+ ]);
158
+ const second = await apply(v2);
159
+ expect(second.failed).toEqual([]);
160
+ expect(second.applied.filter((a) => a.action === "updated").map((a) => a.name)).toEqual([`${S}.touch()`, `${S}.total(numeric,character varying)`, `orders_touch ON ${S}.orders`]);
161
+ expect((await plan(v2)).changes).toEqual([]);
162
+ expect((await admin.query<{ c: string }>(`SELECT obj_description('${S}.total(numeric,varchar)'::regprocedure, 'pg_proc') AS c`))[0]!.c).toContain("stack=e2e3680");
163
+
164
+ // Import: the server's own printing, written back as declarations that parse.
165
+ const live = await readLiveSchema(admin, { schemas: [S] });
166
+ for (const o of live) liveProps(o);
167
+ const [file] = new PostgresGenerator().generate(objectsToIR(live.map((o) => ({ type: o.type, schema: o.schema, name: o.name, ddl: o.statement }))));
168
+ expect(file!.content).toContain("export const touch = func`");
169
+ expect(file!.content).toContain("export const archive = procedure`");
170
+ expect(file!.content).toContain("export const ordersTouch = trigger`");
171
+ expect(file!.content).toContain("EXECUTE FUNCTION ${touch}('x', '1')");
172
+ }, 600_000);
173
+ });
@@ -96,7 +96,8 @@ describe.skipIf(!enabled)("a uuid primary key", () => {
96
96
  await admin!.query("CREATE FUNCTION audit.count_update() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN INSERT INTO audit.updates VALUES (NEW.id); RETURN NEW; END $$");
97
97
  await admin!.query("CREATE TRIGGER audit_update AFTER UPDATE ON uu.tokens FOR EACH ROW EXECUTE FUNCTION audit.count_update()");
98
98
  writeBuild("tokens-v2.json", [UU, tokens("integer")]);
99
- expect((await plan("tokens-v2.json")).changes.map((c) => c.rule)).toEqual(["SQLPG208"]);
99
+ // The application's own trigger is not declared, so the plan also proposes its drop (SQLPG285), which only a prune makes.
100
+ expect((await plan("tokens-v2.json")).changes.map((c) => c.rule)).toEqual(["SQLPG208", "SQLPG285"]);
100
101
 
101
102
  expect(await m.postgresMigrationPlan(args(), undefined, deps())).toMatchObject({ state: "migrate", summary: expect.stringContaining("batched by id") });
102
103
  await m.postgresMigrationExpand(args(), undefined, deps());
@@ -307,7 +307,8 @@ describe.skipIf(!enabled)("an interrupted backfill resumes from its receipts", (
307
307
  writeBuild("events-v2.json", [EV, V2]);
308
308
 
309
309
  // varchar(20) to varchar(10) is a rewrite in place (SQLPG207), which the Op makes in batches instead.
310
- expect((await plan("events-v2.json")).changes.map((c) => c.rule)).toEqual(["SQLPG207"]);
310
+ // The application's own trigger is not declared, so the plan also proposes its drop (SQLPG285), which only a prune makes.
311
+ expect((await plan("events-v2.json")).changes.map((c) => c.rule)).toEqual(["SQLPG207", "SQLPG285"]);
311
312
  expect(await m.postgresMigrationPlan(args(), undefined, deps())).toMatchObject({ state: "migrate", change: "type" });
312
313
  await m.postgresMigrationExpand(args(), undefined, deps());
313
314
  await m.postgresMigrationDualWrite(args(), undefined, deps());
@@ -102,12 +102,13 @@ describe("PostgresMigrationOp", () => {
102
102
  }
103
103
  });
104
104
 
105
- test("every migration step is an activity the sql lexicon exports, and none is a receipt activity", () => {
105
+ test("every migration step is an activity the sql lexicon exports, and the receipt activities are only fallbacks", () => {
106
106
  const exported = new Set(Object.entries(activitiesModule).filter(([, v]) => typeof v === "function").map(([k]) => k));
107
107
  const steps = propsOf(BASE).phases.flatMap((p) => p.steps).filter((s) => s.kind === "activity" && (s as { fn: string }).fn.startsWith("postgresMigration"));
108
108
  expect(steps).toHaveLength(9);
109
109
  for (const s of steps) expect(exported.has((s as { fn: string }).fn)).toBe(true);
110
- expect(exported.has("receiptRead")).toBe(false);
110
+ // The receipt activities are exported for effect() steps only as fallbacks (#3657), so they never take over another lexicon's receipt row.
111
+ expect((activitiesModule as { ACTIVITY_FALLBACKS?: readonly string[] }).ACTIVITY_FALLBACKS).toEqual(["receiptRead", "receiptWrite", "receiptStaleness"]);
111
112
  });
112
113
  });
113
114
 
@@ -27,9 +27,9 @@
27
27
  * stale; onFailure and the contract also delete the migration's receipts.
28
28
  *
29
29
  * The store implements the shared core's `SqlReceiptStore`
30
- * (`../../core/receipts.ts`) over the migration's own connection. It is not
31
- * exported as the `receiptRead` / `receiptWrite` activities, whose names are
32
- * global to a run and belong to an aws or k8s project's receipt row.
30
+ * (`../../core/receipts.ts`) over the migration's own connection, which the
31
+ * backfill reaches directly. A sql project's `effect()` receipts use the same
32
+ * store over `chant_receipts.receipts` (`../../receipts.ts`).
33
33
  */
34
34
 
35
35
  import { OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
@@ -48,10 +48,10 @@ export { receiptAddress } from "../../core/receipts";
48
48
 
49
49
  /** A receipts table chant did not make: a user's table under the same name. */
50
50
  export class ReceiptsTableConflict extends Error {
51
- constructor(schema: string) {
51
+ constructor(schema: string, table: string = POSTGRES_RECEIPTS_TABLE) {
52
52
  super(
53
- `${schema}.${POSTGRES_RECEIPTS_TABLE} exists and is not chant's receipts table (its comment carries no receipts marker). ` +
54
- `The migration keeps its receipts under that name, so it stops rather than write into it; rename the table.`,
53
+ `${schema}.${table} exists and is not chant's receipts table (its comment carries no receipts marker). ` +
54
+ `chant keeps its receipts under that name, so it stops rather than write into it; rename the table.`,
55
55
  );
56
56
  this.name = "ReceiptsTableConflict";
57
57
  }
@@ -70,10 +70,18 @@ export interface PostgresReceiptStore extends SqlReceiptStore {
70
70
  * A receipt store in `<schema>.__chant_receipts`, over `client`. `write` runs
71
71
  * on the client as it is, so a write inside the caller's transaction commits
72
72
  * or rolls back with it. `identity` is the project's ownership stack and env,
73
- * which every address starts with.
73
+ * which every address starts with. `table` names the table in `schema`,
74
+ * `__chant_receipts` by default (`../../receipts.ts` keeps `effect()`
75
+ * receipts in `chant_receipts.receipts`).
74
76
  */
75
- export function postgresReceiptStore(client: PostgresClient, schema: string, identity: { stack?: string; env?: string }, opts: { runId?: string } = {}): PostgresReceiptStore {
76
- const table = `${quoteIdent(schema)}.${quoteIdent(POSTGRES_RECEIPTS_TABLE)}`;
77
+ export function postgresReceiptStore(
78
+ client: PostgresClient,
79
+ schema: string,
80
+ identity: { stack?: string; env?: string },
81
+ opts: { runId?: string; table?: string } = {},
82
+ ): PostgresReceiptStore {
83
+ const name = opts.table ?? POSTGRES_RECEIPTS_TABLE;
84
+ const table = `${quoteIdent(schema)}.${quoteIdent(name)}`;
77
85
  let ensured = false;
78
86
  const comment = async (): Promise<string | null | undefined> => {
79
87
  const [row] = await client.query<{ present: boolean; comment: string | null }>(
@@ -87,7 +95,7 @@ export function postgresReceiptStore(client: PostgresClient, schema: string, ide
87
95
  if (ensured) return true;
88
96
  const c = await comment();
89
97
  if (c === undefined) return false;
90
- if (!hasChantTrailerKey(c ?? undefined, [RECEIPTS_TRAILER_KEY])) throw new ReceiptsTableConflict(schema);
98
+ if (!hasChantTrailerKey(c ?? undefined, [RECEIPTS_TRAILER_KEY])) throw new ReceiptsTableConflict(schema, name);
91
99
  return true;
92
100
  };
93
101
  const address = (receipt: EffectReceiptRef) => receiptAddress(identity, receipt.effect);