@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.
- package/dist/clickhouse/apply/mutations.d.ts +7 -1
- package/dist/clickhouse/apply/mutations.d.ts.map +1 -1
- package/dist/clickhouse/container.d.ts +22 -7
- package/dist/clickhouse/container.d.ts.map +1 -1
- package/dist/clickhouse/plan/deep.d.ts +5 -0
- package/dist/clickhouse/plan/deep.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/backfill.d.ts +11 -5
- package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/op.d.ts +29 -0
- package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/partitions.d.ts +8 -0
- package/dist/clickhouse/rebuild/partitions.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/receipts.d.ts +4 -6
- package/dist/clickhouse/rebuild/receipts.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/shards.d.ts +59 -0
- package/dist/clickhouse/rebuild/shards.d.ts.map +1 -0
- package/dist/clickhouse/rebuild/steps.d.ts +3 -0
- package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/verify.d.ts +4 -0
- package/dist/clickhouse/rebuild/verify.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/core/receipts.d.ts +5 -3
- package/dist/core/receipts.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +5 -5
- package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
- package/dist/lsp/postgres-template.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +15 -0
- package/dist/okf/index.md +3 -0
- package/dist/okf/types/PostgresFunction.md +9 -0
- package/dist/okf/types/PostgresProcedure.md +9 -0
- package/dist/okf/types/PostgresTrigger.md +9 -0
- package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +13 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/sql-exec.d.ts +41 -0
- package/dist/op/activities/sql-exec.d.ts.map +1 -0
- package/dist/op/activity-contracts.d.ts +20 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/apply/apply.d.ts +6 -1
- package/dist/postgres/apply/apply.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +9 -2
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +110 -3
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +3 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/live-export.d.ts.map +1 -1
- package/dist/postgres/live/catalog.d.ts +17 -5
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts +6 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/migrate/receipts.d.ts +8 -5
- package/dist/postgres/migrate/receipts.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +82 -5
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts +1 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +21 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +6 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +11 -1
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/receipts.d.ts +102 -0
- package/dist/receipts.d.ts.map +1 -0
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sqlpg001.ts +3 -0
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clickhouse/apply/mutations.ts +9 -3
- package/src/clickhouse/container.ts +39 -11
- package/src/clickhouse/plan/cluster-drift.e2e.test.ts +117 -0
- package/src/clickhouse/plan/deep.ts +102 -3
- package/src/clickhouse/plan/normalize.ts +10 -0
- package/src/clickhouse/plan/plan.test.ts +10 -0
- package/src/clickhouse/rebuild/backfill.ts +85 -42
- package/src/clickhouse/rebuild/op.ts +80 -35
- package/src/clickhouse/rebuild/partitions.ts +11 -0
- package/src/clickhouse/rebuild/rebuild-sharded.e2e.test.ts +161 -0
- package/src/clickhouse/rebuild/rebuild.e2e.test.ts +110 -4
- package/src/clickhouse/rebuild/rebuild.test.ts +56 -4
- package/src/clickhouse/rebuild/receipts.ts +4 -6
- package/src/clickhouse/rebuild/shards.ts +104 -0
- package/src/clickhouse/rebuild/steps.ts +4 -0
- package/src/clickhouse/rebuild/verify.ts +14 -5
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +3 -0
- package/src/config.ts +7 -0
- package/src/core/receipts.ts +5 -3
- package/src/generated/lexicon-sql.json +15 -0
- package/src/index.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlpg001.ts +3 -0
- package/src/lsp/postgres-template.ts +6 -0
- package/src/mcp/postgres.ts +1 -1
- package/src/op/activities/clickhouse-rebuild.ts +32 -7
- package/src/op/activities/index.ts +21 -4
- package/src/op/activities/sql-exec.ts +84 -0
- package/src/op/activity-contracts.ts +15 -0
- package/src/op/effect-receipts.e2e.test.ts +277 -0
- package/src/plugin.ts +3 -0
- package/src/postgres/apply/apply.ts +21 -5
- package/src/postgres/apply/statements.ts +53 -11
- package/src/postgres/entities.ts +332 -9
- package/src/postgres/entity-types.ts +3 -0
- package/src/postgres/import/generator.ts +5 -2
- package/src/postgres/import/live-export.ts +4 -0
- package/src/postgres/live/catalog.ts +81 -5
- package/src/postgres/live/describe-resources.ts +17 -7
- package/src/postgres/live/routines.e2e.test.ts +173 -0
- package/src/postgres/migrate/batches.e2e.test.ts +2 -1
- package/src/postgres/migrate/migrate.e2e.test.ts +2 -1
- package/src/postgres/migrate/migrate.test.ts +3 -2
- package/src/postgres/migrate/receipts.ts +18 -10
- package/src/postgres/parser.ts +314 -8
- package/src/postgres/plan/commands.ts +2 -2
- package/src/postgres/plan/deep.ts +57 -17
- package/src/postgres/plan/diff.ts +46 -6
- package/src/postgres/plan/normalize.ts +158 -4
- package/src/postgres/plan/rules.ts +36 -0
- package/src/postgres/plan/schema.ts +25 -6
- package/src/postgres/plan/server-normalize.ts +24 -0
- package/src/postgres/routines.test.ts +150 -0
- package/src/postgres.ts +11 -0
- package/src/receipts.test.ts +91 -0
- package/src/receipts.ts +259 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres.md +18 -2
- package/src/skills/chant-sql-rebuild.md +5 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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}.${
|
|
54
|
-
`
|
|
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(
|
|
76
|
-
|
|
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);
|