@intentius/chant-lexicon-sql 0.115.0 → 0.117.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/codegen/registry.d.ts.map +1 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.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 +35 -0
- package/dist/okf/index.md +7 -0
- package/dist/okf/types/PostgresDefaultPrivileges.md +9 -0
- package/dist/okf/types/PostgresFunction.md +9 -0
- package/dist/okf/types/PostgresGrant.md +9 -0
- package/dist/okf/types/PostgresPolicy.md +9 -0
- package/dist/okf/types/PostgresProcedure.md +9 -0
- package/dist/okf/types/PostgresRole.md +9 -0
- package/dist/okf/types/PostgresTrigger.md +9 -0
- package/dist/op/activities/postgres-migration.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +10 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/access/acl.d.ts +119 -0
- package/dist/postgres/access/acl.d.ts.map +1 -0
- package/dist/postgres/access/import.d.ts +13 -0
- package/dist/postgres/access/import.d.ts.map +1 -0
- package/dist/postgres/access/live.d.ts +24 -0
- package/dist/postgres/access/live.d.ts.map +1 -0
- 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 +12 -2
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +212 -3
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +7 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts +4 -1
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/live-export.d.ts +6 -0
- package/dist/postgres/import/live-export.d.ts.map +1 -1
- package/dist/postgres/live/bind.d.ts +2 -0
- package/dist/postgres/live/bind.d.ts.map +1 -1
- package/dist/postgres/live/catalog.d.ts +25 -4
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts +15 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/migrate/op.d.ts +29 -0
- package/dist/postgres/migrate/op.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +155 -5
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/commands.d.ts +12 -0
- package/dist/postgres/plan/commands.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts +11 -2
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +28 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +14 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +12 -1
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/plan/server-normalize.d.ts +4 -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/rules/postgres-templates.ts +2 -2
- package/dist/rules/sqlpg001.ts +10 -4
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +7 -0
- package/src/config.ts +10 -0
- package/src/generated/lexicon-sql.json +35 -0
- package/src/index.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlpg001.ts +10 -4
- package/src/lsp/postgres-template.ts +8 -0
- package/src/mcp/postgres.ts +1 -1
- package/src/migration-statements.ts +7 -0
- package/src/op/activities/postgres-migration.ts +27 -2
- package/src/op/activity-contracts.ts +1 -0
- package/src/plugin.ts +6 -0
- package/src/postgres/access/access.e2e.test.ts +191 -0
- package/src/postgres/access/access.test.ts +147 -0
- package/src/postgres/access/acl.ts +400 -0
- package/src/postgres/access/import.ts +64 -0
- package/src/postgres/access/live.ts +123 -0
- package/src/postgres/apply/apply.ts +135 -6
- package/src/postgres/apply/statements.ts +104 -16
- package/src/postgres/entities.ts +613 -14
- package/src/postgres/entity-types.ts +7 -0
- package/src/postgres/import/generator.ts +18 -5
- package/src/postgres/import/live-export.ts +16 -2
- package/src/postgres/live/bind.ts +3 -0
- package/src/postgres/live/catalog.ts +155 -5
- package/src/postgres/live/describe-resources.ts +43 -9
- 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 +24 -1
- package/src/postgres/migrate/op.ts +80 -35
- package/src/postgres/migrate/outer.e2e.test.ts +163 -0
- package/src/postgres/parser.ts +554 -9
- package/src/postgres/plan/commands.ts +61 -8
- package/src/postgres/plan/deep.ts +79 -18
- package/src/postgres/plan/diff.ts +114 -9
- package/src/postgres/plan/normalize.ts +272 -5
- package/src/postgres/plan/rules.ts +80 -0
- package/src/postgres/plan/schema.ts +27 -6
- package/src/postgres/plan/server-normalize.ts +53 -1
- package/src/postgres/routines.test.ts +150 -0
- package/src/postgres.ts +22 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres-migration.md +3 -1
- package/src/skills/chant-sql-postgres.md +25 -2
- package/src/validate.ts +7 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Functions, procedures and triggers (#3680): what the tags parse and refuse,
|
|
3
|
+
* how a change to each is classified and made, and how an import writes them.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { func, procedure, schema, SqlTemplateError, table, trigger } from "./entities";
|
|
8
|
+
import { diffObject, keyedByQualifiedName, pgSchemaFromLive, type PgSchemaObject } from "./plan/schema";
|
|
9
|
+
import { diffPgSchemas, matchPgObjects } from "./plan/diff";
|
|
10
|
+
import { alterSteps, dropStatement } from "./apply/statements";
|
|
11
|
+
import { declaredObjects } from "./apply/apply";
|
|
12
|
+
import type { LivePgObject } from "./live/catalog";
|
|
13
|
+
import { objectsToIR } from "./import/ir";
|
|
14
|
+
import { PostgresGenerator } from "./import/generator";
|
|
15
|
+
|
|
16
|
+
const app = schema`CREATE SCHEMA app`;
|
|
17
|
+
const users = table`CREATE TABLE ${app}.users (id bigint PRIMARY KEY, email text, updated_at timestamptz)`;
|
|
18
|
+
|
|
19
|
+
describe("the tags", () => {
|
|
20
|
+
test("a function: its signature, attributes and body as written; an interpolation in the body is a reference", () => {
|
|
21
|
+
const count = func`
|
|
22
|
+
CREATE FUNCTION ${app}.user_count(min_id int = 0, OUT n bigint)
|
|
23
|
+
LANGUAGE sql STABLE STRICT PARALLEL SAFE SET search_path = ${app}
|
|
24
|
+
AS $$ SELECT count(*) FROM ${users} WHERE ${users.columns.id} >= min_id $$;
|
|
25
|
+
COMMENT ON FUNCTION ${app}.user_count(int) IS 'Users'`;
|
|
26
|
+
expect(count.entityType).toBe("Postgres::Function");
|
|
27
|
+
expect(count.sqlName).toBe("app.user_count");
|
|
28
|
+
expect(count.props).toMatchObject({
|
|
29
|
+
schema: "app",
|
|
30
|
+
name: "user_count",
|
|
31
|
+
args: [
|
|
32
|
+
{ mode: "in", name: "min_id", type: "int", default: "0" },
|
|
33
|
+
{ mode: "out", name: "n", type: "bigint" },
|
|
34
|
+
],
|
|
35
|
+
language: "sql",
|
|
36
|
+
volatility: "stable",
|
|
37
|
+
strict: true,
|
|
38
|
+
parallel: "safe",
|
|
39
|
+
set: { search_path: "app" },
|
|
40
|
+
body: " SELECT count(*) FROM app.users WHERE id >= min_id ",
|
|
41
|
+
comment: "Users",
|
|
42
|
+
});
|
|
43
|
+
expect(count.dependsOn).toContain(users);
|
|
44
|
+
expect(count.props.reads).toEqual([users]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("a procedure and a trigger", () => {
|
|
48
|
+
const archive = procedure`CREATE PROCEDURE ${app}.archive(IN before bigint, INOUT n int DEFAULT NULL) LANGUAGE plpgsql AS 'BEGIN n := 0; END'`;
|
|
49
|
+
expect(archive.props).toMatchObject({ args: [{ mode: "in", name: "before" }, { mode: "inout", name: "n", default: "NULL" }], body: "BEGIN n := 0; END" });
|
|
50
|
+
const touch = func`CREATE FUNCTION ${app}.touch() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at := now(); RETURN NEW; END $$`;
|
|
51
|
+
const t = trigger`CREATE TRIGGER users_touch BEFORE INSERT OR UPDATE OF email ON ${users} FOR EACH ROW WHEN (NEW.email IS NOT NULL) EXECUTE FUNCTION ${touch}('x', 1)`;
|
|
52
|
+
expect(t.entityType).toBe("Postgres::Trigger");
|
|
53
|
+
expect(t.props).toMatchObject({
|
|
54
|
+
schema: "app",
|
|
55
|
+
name: "users_touch",
|
|
56
|
+
timing: "before",
|
|
57
|
+
events: [{ event: "insert" }, { event: "update", columns: ["email"] }],
|
|
58
|
+
tableName: "app.users",
|
|
59
|
+
forEach: "row",
|
|
60
|
+
when: "NEW.email IS NOT NULL",
|
|
61
|
+
functionName: "app.touch",
|
|
62
|
+
args: ["'x'", "1"],
|
|
63
|
+
});
|
|
64
|
+
expect(t.dependsOn).toEqual([users, touch]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a SQL-standard body, SET FROM CURRENT and a qualified trigger name are refused", () => {
|
|
68
|
+
expect(() => func`CREATE FUNCTION app.f(a int) RETURNS int LANGUAGE sql RETURN a + 1`).toThrow(/SQL-standard body/);
|
|
69
|
+
expect(() => func`CREATE FUNCTION app.f() RETURNS int LANGUAGE sql BEGIN ATOMIC SELECT 1; END`).toThrow(/SQL-standard body/);
|
|
70
|
+
expect(() => func`CREATE FUNCTION app.f() RETURNS int LANGUAGE sql SET search_path FROM CURRENT AS 'select 1'`).toThrow(/FROM CURRENT/);
|
|
71
|
+
expect(() => procedure`CREATE PROCEDURE app.p() LANGUAGE sql STABLE AS 'select 1'`).toThrow(/not an attribute of a procedure/);
|
|
72
|
+
expect(() => trigger`CREATE TRIGGER app.t BEFORE INSERT ON app.users EXECUTE FUNCTION app.f()`).toThrow(/not schema-qualified/);
|
|
73
|
+
expect(() => func`CREATE TABLE app.t (a int)`).toThrow(SqlTemplateError);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── Classifying and making changes ─────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
const F = "Postgres::Function";
|
|
80
|
+
const TR = "Postgres::Trigger";
|
|
81
|
+
const live = (type: string, statement: string, extra: Partial<LivePgObject> = {}) => pgSchemaFromLive([{ type, statement, name: "", oid: "1", ...extra } as LivePgObject], "public");
|
|
82
|
+
const declared = (type: string, ddl: string): PgSchemaObject[] => keyedByQualifiedName([{ key: "x", canonical: { ...diffObject(type, ddl, "public"), exportName: "x" } }]);
|
|
83
|
+
const rules = (type: string, before: string, after: string, extra: Partial<LivePgObject> = {}) =>
|
|
84
|
+
diffPgSchemas(live(type, before, extra), declared(type, after)).changes.map((c) => [c.field, c.rule]);
|
|
85
|
+
|
|
86
|
+
const FN = (body: string, returns = "integer", args = "a integer") => `CREATE FUNCTION app.f(${args}) RETURNS ${returns} LANGUAGE sql AS $$ ${body} $$`;
|
|
87
|
+
const SERVER_FN = "CREATE OR REPLACE FUNCTION app.f(a integer)\n RETURNS integer\n LANGUAGE sql\nAS $function$ select a $function$\n";
|
|
88
|
+
|
|
89
|
+
describe("the classifier", () => {
|
|
90
|
+
test("the server's printing of a function is the declaration's", () => {
|
|
91
|
+
expect(rules(F, SERVER_FN, FN("select a", "int", "a int"))).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a new body or attribute is CREATE OR REPLACE; a new result is drop and create, or expand and contract when something depends on it", () => {
|
|
95
|
+
expect(rules(F, SERVER_FN, FN("select a + 1"))).toEqual([["body", "SQLPG280"]]);
|
|
96
|
+
expect(rules(F, SERVER_FN, FN("select a", "bigint"))).toEqual([["returns", "SQLPG281"]]);
|
|
97
|
+
expect(rules(F, SERVER_FN, FN("select a", "integer", "b integer"))).toEqual([["args", "SQLPG281"]]);
|
|
98
|
+
expect(rules(F, SERVER_FN, FN("select a", "bigint"), { dependents: ["view app.v"] })).toEqual([["returns", "SQLPG282"]]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a trigger: created on an existing table under SHARE ROW EXCLUSIVE, changed in place, dropped", () => {
|
|
102
|
+
const T1 = "CREATE TRIGGER t BEFORE UPDATE ON app.users FOR EACH ROW EXECUTE FUNCTION app.touch()";
|
|
103
|
+
const T2 = "CREATE TRIGGER t BEFORE INSERT OR UPDATE ON app.users FOR EACH ROW EXECUTE FUNCTION app.touch()";
|
|
104
|
+
expect(diffPgSchemas([], declared(TR, T1)).changes.map((c) => c.rule)).toEqual(["SQLPG283"]);
|
|
105
|
+
expect(rules(TR, T1, T2)).toEqual([["events", "SQLPG284"]]);
|
|
106
|
+
expect(diffPgSchemas(live(TR, T1), []).changes.map((c) => [c.rule, c.class])).toEqual([["SQLPG285", "drop"]]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function steps(type: string, liveDdl: string, declaredDdl: string): string[] {
|
|
111
|
+
const [obj] = declaredObjects(JSON.stringify({ dialect: "postgres", objects: [{ export: "x", type, ddl: declaredDdl, dependsOn: [] }] }));
|
|
112
|
+
const after = keyedByQualifiedName([{ key: "x", canonical: obj!.canonical }]);
|
|
113
|
+
const before = live(type, liveDdl);
|
|
114
|
+
const m = matchPgObjects(before, after).find((x) => x.kind === "matched")!;
|
|
115
|
+
return alterSteps(obj!, m.before!.canonical, diffPgSchemas(before, after).changes, { major: 18 }).steps.map((s) => s.sql);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe("the statements", () => {
|
|
119
|
+
test("OR REPLACE, or a drop and a create named by the parameter types", () => {
|
|
120
|
+
expect(steps(F, SERVER_FN, FN("select a + 1"))).toEqual(["CREATE OR REPLACE FUNCTION app.f(a integer) RETURNS integer LANGUAGE sql AS $$ select a + 1 $$"]);
|
|
121
|
+
const recreate = steps(F, SERVER_FN, FN("select a", "bigint"));
|
|
122
|
+
expect(recreate[0]).toBe("DROP FUNCTION app.f(integer)");
|
|
123
|
+
expect(recreate[1]).toBe("CREATE FUNCTION app.f(a integer) RETURNS bigint LANGUAGE sql AS $$ select a $$");
|
|
124
|
+
expect(recreate[2]).toMatch(/^COMMENT ON FUNCTION app\.f\(integer\) IS /);
|
|
125
|
+
expect(dropStatement("procedure", "app", "p", "(integer,text)").sql).toBe("DROP PROCEDURE app.p(integer,text)");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a trigger is replaced in place; a constraint trigger is dropped and created", () => {
|
|
129
|
+
const T1 = "CREATE TRIGGER t BEFORE UPDATE ON app.users FOR EACH ROW EXECUTE FUNCTION app.touch()";
|
|
130
|
+
expect(steps(TR, T1, T1.replace("UPDATE", "INSERT"))).toEqual(["CREATE OR REPLACE TRIGGER t BEFORE INSERT ON app.users FOR EACH ROW EXECUTE FUNCTION app.touch()"]);
|
|
131
|
+
const C1 = "CREATE CONSTRAINT TRIGGER t AFTER UPDATE ON app.users DEFERRABLE FOR EACH ROW EXECUTE FUNCTION app.touch()";
|
|
132
|
+
expect(steps(TR, C1, C1.replace("UPDATE", "INSERT")).slice(0, 2)).toEqual(["DROP TRIGGER t ON app.users", C1.replace("UPDATE", "INSERT")]);
|
|
133
|
+
expect(dropStatement("trigger", "app", "t", " ON app.users").sql).toBe("DROP TRIGGER t ON app.users");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("importing", () => {
|
|
138
|
+
test("functions and triggers are written as func and trigger declarations, their references interpolated", () => {
|
|
139
|
+
const content = new PostgresGenerator().generate(
|
|
140
|
+
objectsToIR([
|
|
141
|
+
{ type: "Postgres::Table", schema: "app", name: "users", ddl: "CREATE TABLE app.users (\n id bigint\n)" },
|
|
142
|
+
{ type: "Postgres::Function", schema: "app", name: "touch", ddl: "CREATE OR REPLACE FUNCTION app.touch()\n RETURNS trigger\n LANGUAGE plpgsql\nAS $function$ BEGIN RETURN NEW; END $function$" },
|
|
143
|
+
{ type: "Postgres::Trigger", schema: "app", name: "users_touch", ddl: "CREATE TRIGGER users_touch BEFORE UPDATE ON app.users FOR EACH ROW EXECUTE FUNCTION app.touch()" },
|
|
144
|
+
]),
|
|
145
|
+
)[0]!.content;
|
|
146
|
+
expect(content).toContain('import { func, table, trigger } from "@intentius/chant-lexicon-sql/postgres";');
|
|
147
|
+
expect(content).toContain("export const touch = func`");
|
|
148
|
+
expect(content).toContain("ON ${users} FOR EACH ROW EXECUTE FUNCTION ${touch}()");
|
|
149
|
+
});
|
|
150
|
+
});
|
package/src/postgres.ts
CHANGED
|
@@ -15,6 +15,12 @@ export {
|
|
|
15
15
|
type,
|
|
16
16
|
domain,
|
|
17
17
|
extension,
|
|
18
|
+
func,
|
|
19
|
+
procedure,
|
|
20
|
+
trigger,
|
|
21
|
+
policy,
|
|
22
|
+
role,
|
|
23
|
+
grant,
|
|
18
24
|
literal,
|
|
19
25
|
SqlLiteral,
|
|
20
26
|
SqlTemplateError,
|
|
@@ -33,6 +39,12 @@ export {
|
|
|
33
39
|
type PostgresEnum,
|
|
34
40
|
type PostgresDomain,
|
|
35
41
|
type PostgresExtension,
|
|
42
|
+
type PostgresFunction,
|
|
43
|
+
type PostgresProcedure,
|
|
44
|
+
type PostgresTrigger,
|
|
45
|
+
type PostgresPolicy,
|
|
46
|
+
type PostgresRole,
|
|
47
|
+
type PostgresGrant,
|
|
36
48
|
type ColumnDef,
|
|
37
49
|
type KeyDef,
|
|
38
50
|
type CheckDef,
|
|
@@ -46,6 +58,16 @@ export {
|
|
|
46
58
|
type EnumProps,
|
|
47
59
|
type DomainProps,
|
|
48
60
|
type ExtensionProps,
|
|
61
|
+
type RoutineProps,
|
|
62
|
+
type RoutineArgDef,
|
|
63
|
+
type FunctionProps,
|
|
64
|
+
type ProcedureProps,
|
|
65
|
+
type TriggerProps,
|
|
66
|
+
type PolicyProps,
|
|
67
|
+
type RoleProps,
|
|
68
|
+
type GrantProps,
|
|
69
|
+
type DefaultPrivilegesProps,
|
|
70
|
+
type PrivilegeDef,
|
|
49
71
|
type LineageEdge,
|
|
50
72
|
} from "./postgres/entities";
|
|
51
73
|
|
package/src/skill-defs.ts
CHANGED
|
@@ -54,7 +54,7 @@ export const sqlSkills = createSkillsLoader(import.meta.url, [
|
|
|
54
54
|
file: "chant-sql-postgres.md",
|
|
55
55
|
name: "chant-sql-postgres",
|
|
56
56
|
description:
|
|
57
|
-
"Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types and
|
|
57
|
+
"Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types, extensions, functions, procedures, triggers, policies, roles and grants as SQL-shaped tagged templates, with references, lineage and the SQLPG lint rules",
|
|
58
58
|
triggers: [
|
|
59
59
|
{ type: "context" as const, value: "postgres" },
|
|
60
60
|
{ type: "context" as const, value: "postgres schema" },
|
|
@@ -40,6 +40,8 @@ Options besides `name`, `env`, `table`, `column`, `using`:
|
|
|
40
40
|
- `gate` and `contractGate`: the two approvals, each with `gate` (name), `timeout` and `description`; `gate` also takes `approval` for quorum, roles and a policy, and a policy gets `verifiedRows` and `mismatched` as context;
|
|
41
41
|
- `backfillTimeout` (default `6h`), `output` (default `dist/schema.json`), `path`, `build` (default true runs `chant build` first), `stack`, `ownershipEnv`.
|
|
42
42
|
|
|
43
|
+
Run as one step of a change approved elsewhere: `gates: "outer"` leaves out the switch gate, the contract gate and the Contract phase (verification still runs; the old column is kept until its retention date), and `onFailure: "keep"` leaves out onFailure, so a failed run's next run resumes the backfill from its receipts. Set them rather than editing the Op's phases.
|
|
44
|
+
|
|
43
45
|
## What it does
|
|
44
46
|
|
|
45
47
|
| Phase | Step |
|
|
@@ -103,7 +105,7 @@ or set `replicationLag: false` to run without the check.
|
|
|
103
105
|
|
|
104
106
|
## When something goes wrong
|
|
105
107
|
|
|
106
|
-
- A failed step (a value the `using` expression cannot convert, a verification mismatch, a refused declaration, an index that cannot be built) runs onFailure: the trigger, its function, the check, the carried indexes and constraints, the new column and the receipts table are dropped, and the plan shows the original change again. The old column was never touched. Fix the declaration or the data and run again. A failed gate or an aborted run does not run onFailure.
|
|
108
|
+
- A failed step (a value the `using` expression cannot convert, a verification mismatch, a refused declaration, an index that cannot be built) runs onFailure, unless the Op sets `onFailure: "keep"`: the trigger, its function, the check, the carried indexes and constraints, the new column and the receipts table are dropped, and the plan shows the original change again. The old column was never touched. Fix the declaration or the data and run again. A failed gate or an aborted run does not run onFailure.
|
|
107
109
|
- A lock timeout means another session holds a lock the step needs (SQLSTATE 55P03). The step retries a few times, then fails. Run again when the blocker has finished.
|
|
108
110
|
- The working objects (new column, trigger, function, check, receipts table) carry chant's comment marker with a `migration=` key, and plans, imports and prunes leave them out, so an `ApplyOp` for the rest of the schema can run beside a migration. An object under one of the working names that is not this migration's stops the run; rename or drop it by hand.
|
|
109
111
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
skill: chant-sql-postgres
|
|
3
|
-
description: Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types and
|
|
3
|
+
description: Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types, extensions, functions, procedures, triggers, policies, roles and grants as SQL-shaped tagged templates, with references, lineage and the SQLPG lint rules
|
|
4
4
|
user-invocable: true
|
|
5
5
|
---
|
|
6
6
|
# Declaring a Postgres schema with chant
|
|
@@ -37,6 +37,12 @@ The export name (`users`) is the object's identity in chant. The name in the SQL
|
|
|
37
37
|
| `type` | `CREATE TYPE ... AS ENUM` | `Postgres::Enum` |
|
|
38
38
|
| `domain` | `CREATE DOMAIN` | `Postgres::Domain` |
|
|
39
39
|
| `extension` | `CREATE EXTENSION` | `Postgres::Extension` |
|
|
40
|
+
| `func` | `CREATE FUNCTION` | `Postgres::Function` |
|
|
41
|
+
| `procedure` | `CREATE PROCEDURE` | `Postgres::Procedure` |
|
|
42
|
+
| `trigger` | `CREATE TRIGGER` | `Postgres::Trigger` |
|
|
43
|
+
| `policy` | `CREATE POLICY` | `Postgres::Policy` |
|
|
44
|
+
| `role` | `CREATE ROLE` (no password, no memberships) | `Postgres::Role` |
|
|
45
|
+
| `grant` | `GRANT`, `REVOKE`, `ALTER DEFAULT PRIVILEGES` | `Postgres::Grant`, `Postgres::DefaultPrivileges` |
|
|
40
46
|
|
|
41
47
|
`table` and `view` imported from the package root are ClickHouse's. Import every tag from `@intentius/chant-lexicon-sql/postgres`.
|
|
42
48
|
|
|
@@ -124,6 +130,23 @@ export const citext = extension`CREATE EXTENSION IF NOT EXISTS citext WITH SCHEM
|
|
|
124
130
|
|
|
125
131
|
Use the entity as a column type: `status ${status} NOT NULL DEFAULT 'placed'`. The table then depends on the type. Enum labels are values of the type, so a label cannot be removed or reordered later without a rebuild; add labels at the end. An extension is an object of its own with no references to it from a column type, so a table using a type it provides does not wait for it; keep extensions in a project the database already has, or apply them first.
|
|
126
132
|
|
|
133
|
+
## Functions, procedures and triggers
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { func, trigger } from "@intentius/chant-lexicon-sql/postgres";
|
|
137
|
+
|
|
138
|
+
export const touch = func`CREATE FUNCTION ${app}.touch() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
139
|
+
BEGIN NEW.updated_at := now(); RETURN NEW; END
|
|
140
|
+
$$`;
|
|
141
|
+
export const ordersTouch = trigger`CREATE TRIGGER orders_touch BEFORE UPDATE ON ${orders} FOR EACH ROW EXECUTE FUNCTION ${touch}()`;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The tag is `func` (`function` is a JavaScript key word). Write the body as a string, `AS $$ ... $$`: it is kept and compared verbatim. `${users}` inside the body renders as `app.users` and makes the routine depend on the table. A SQL-standard body (`RETURN ...`, `BEGIN ATOMIC`) and `SET ... FROM CURRENT` are SQLPG001 errors. Overloads are separate objects, identified by their input parameter types. A trigger's name is unqualified; it lives on its table.
|
|
145
|
+
|
|
146
|
+
## Access
|
|
147
|
+
|
|
148
|
+
A table's template may end with `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`. `policy`, `role` and `grant` declare policies, the roles the schema needs (never a password or a membership: those are the environment's), privileges on schemas, tables, columns, sequences and routines, and default privileges. They are planned and applied only where `sql.profiles.<env>.access` is true; there, the declared privileges are the whole of them, and one granted by hand is revoked. A role the environment provisions is named as text (`TO support`), never declared. `GRANT ... ON ALL TABLES IN SCHEMA` is refused: grant per table and declare default privileges.
|
|
149
|
+
|
|
127
150
|
## Comments
|
|
128
151
|
|
|
129
152
|
Postgres has no inline comment clause. Follow the `CREATE` with `COMMENT ON` for the object, its columns and its named constraints, separated by `;`:
|
|
@@ -162,4 +185,4 @@ chant build src --lexicon sql -o dist/schema.json
|
|
|
162
185
|
|
|
163
186
|
## Not covered
|
|
164
187
|
|
|
165
|
-
`ALTER
|
|
188
|
+
`ALTER` (other than a table's row-level security), role memberships, rules, aggregates, event triggers and the composite and range forms of `CREATE TYPE` are not declared with the tags. Planning a schema change (`chant sql diff`, `chant sql plan`, the lock classes) is the `chant-sql-postgres-plan` skill. Apply and the expand-and-contract migration Op have their own skills, which arrive with the code they describe.
|
package/src/validate.ts
CHANGED
|
@@ -34,6 +34,13 @@ const REQUIRED_ENTITIES = [
|
|
|
34
34
|
"PostgresEnum",
|
|
35
35
|
"PostgresDomain",
|
|
36
36
|
"PostgresExtension",
|
|
37
|
+
"PostgresFunction",
|
|
38
|
+
"PostgresProcedure",
|
|
39
|
+
"PostgresTrigger",
|
|
40
|
+
"PostgresPolicy",
|
|
41
|
+
"PostgresRole",
|
|
42
|
+
"PostgresGrant",
|
|
43
|
+
"PostgresDefaultPrivileges",
|
|
37
44
|
];
|
|
38
45
|
|
|
39
46
|
/** Engines a ClickHouse schema declares most: the MergeTree family and the engines beside it. */
|