@intentius/chant-lexicon-sql 0.116.0 → 0.118.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/access.d.ts +132 -0
- package/dist/clickhouse/access.d.ts.map +1 -0
- package/dist/clickhouse/apply/apply.d.ts.map +1 -1
- package/dist/clickhouse/apply/statements.d.ts +2 -0
- package/dist/clickhouse/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +171 -2
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/http.d.ts +3 -0
- package/dist/clickhouse/http.d.ts.map +1 -1
- package/dist/clickhouse/import/generator.d.ts +28 -0
- package/dist/clickhouse/import/generator.d.ts.map +1 -1
- package/dist/clickhouse/import/ir.d.ts.map +1 -1
- package/dist/clickhouse/import/parser.d.ts +3 -3
- package/dist/clickhouse/import/parser.d.ts.map +1 -1
- package/dist/clickhouse/live/bind.d.ts.map +1 -1
- package/dist/clickhouse/live/catalog.d.ts +41 -6
- package/dist/clickhouse/live/catalog.d.ts.map +1 -1
- package/dist/clickhouse/live/describe-resources.d.ts +3 -2
- package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
- package/dist/clickhouse/parser.d.ts +54 -3
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/plan/commands.d.ts +3 -1
- package/dist/clickhouse/plan/commands.d.ts.map +1 -1
- package/dist/clickhouse/plan/deep.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.d.ts +12 -0
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts +61 -1
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/plan/report.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.d.ts +7 -0
- package/dist/clickhouse/plan/rules.d.ts.map +1 -1
- package/dist/clickhouse/plan/schema.d.ts +8 -4
- package/dist/clickhouse/plan/schema.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/backfill.d.ts +3 -1
- package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/observe.d.ts +7 -3
- package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/op.d.ts +8 -0
- package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/steps.d.ts +48 -0
- package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
- package/dist/clickhouse/testing/fake-server.d.ts.map +1 -1
- package/dist/clickhouse/testing/writable-server.d.ts.map +1 -1
- package/dist/clickhouse/topology.d.ts.map +1 -1
- package/dist/clickhouse.d.ts +1 -1
- package/dist/clickhouse.d.ts.map +1 -1
- package/dist/codegen/classifier-page.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/config.d.ts +70 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/core/references.d.ts +4 -2
- package/dist/core/references.d.ts.map +1 -1
- package/dist/core/statements.d.ts +11 -0
- package/dist/core/statements.d.ts.map +1 -1
- package/dist/files/build-root.d.ts +26 -0
- package/dist/files/build-root.d.ts.map +1 -0
- package/dist/files/clickhouse.d.ts +19 -0
- package/dist/files/clickhouse.d.ts.map +1 -0
- package/dist/files/common.d.ts +25 -0
- package/dist/files/common.d.ts.map +1 -0
- package/dist/files/postgres.d.ts +36 -0
- package/dist/files/postgres.d.ts.map +1 -0
- package/dist/import-parser.d.ts +25 -0
- package/dist/import-parser.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +8 -8
- package/dist/lint/rules/sqlch001.d.ts +1 -1
- package/dist/lint/rules/sqlch001.d.ts.map +1 -1
- package/dist/lint/rules/sqlch002.d.ts.map +1 -1
- package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
- package/dist/lint/rules/templates.d.ts +1 -1
- package/dist/lint/rules/templates.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 +50 -0
- package/dist/okf/index.md +11 -1
- package/dist/okf/rules/SQLCH001.md +2 -2
- package/dist/okf/types/Dictionary.md +9 -0
- package/dist/okf/types/Function.md +9 -0
- package/dist/okf/types/Grant.md +9 -0
- package/dist/okf/types/PostgresDefaultPrivileges.md +9 -0
- package/dist/okf/types/PostgresGrant.md +9 -0
- package/dist/okf/types/PostgresPolicy.md +9 -0
- package/dist/okf/types/PostgresRole.md +9 -0
- package/dist/okf/types/Role.md +9 -0
- package/dist/okf/types/RowPolicy.md +9 -0
- package/dist/okf/types/User.md +9 -0
- package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
- package/dist/op/activities/postgres-migration.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +19 -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.map +1 -1
- package/dist/postgres/apply/statements.d.ts +71 -0
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +106 -4
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +4 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts +37 -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 +11 -2
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/client.d.ts +3 -0
- package/dist/postgres/live/client.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts +9 -0
- 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 +76 -3
- 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 +10 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +8 -1
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +8 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +2 -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/sqlch001.ts +26 -4
- package/dist/rules/sqlch002.ts +3 -2
- package/dist/rules/sqlpg001.ts +7 -4
- package/dist/rules/templates.ts +2 -2
- package/dist/serializer.d.ts +2 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/sql-files.d.ts +79 -0
- package/dist/sql-files.d.ts.map +1 -0
- package/dist/token-source.d.ts +88 -0
- package/dist/token-source.d.ts.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clickhouse/access.test.ts +203 -0
- package/src/clickhouse/access.ts +595 -0
- package/src/clickhouse/apply/apply.ts +29 -20
- package/src/clickhouse/apply/statements.ts +105 -8
- package/src/clickhouse/dictionary.test.ts +177 -0
- package/src/clickhouse/entities.ts +356 -6
- package/src/clickhouse/function.test.ts +91 -0
- package/src/clickhouse/http.ts +11 -2
- package/src/clickhouse/import/generator.ts +81 -46
- package/src/clickhouse/import/import.test.ts +9 -3
- package/src/clickhouse/import/ir.ts +6 -2
- package/src/clickhouse/import/parser.ts +10 -18
- package/src/clickhouse/live/access.e2e.test.ts +218 -0
- package/src/clickhouse/live/bind.ts +17 -6
- package/src/clickhouse/live/catalog.ts +106 -10
- package/src/clickhouse/live/describe-resources.ts +23 -10
- package/src/clickhouse/live/dictionaries.e2e.test.ts +186 -0
- package/src/clickhouse/live/functions.e2e.test.ts +155 -0
- package/src/clickhouse/live/live.test.ts +2 -2
- package/src/clickhouse/parser.ts +136 -4
- package/src/clickhouse/plan/commands.ts +18 -7
- package/src/clickhouse/plan/deep.ts +71 -11
- package/src/clickhouse/plan/diff.ts +65 -2
- package/src/clickhouse/plan/normalize.ts +200 -6
- package/src/clickhouse/plan/report.ts +14 -3
- package/src/clickhouse/plan/rules.ts +44 -1
- package/src/clickhouse/plan/schema.ts +40 -17
- package/src/clickhouse/plan/server-format.ts +1 -1
- package/src/clickhouse/rebuild/backfill.ts +5 -25
- package/src/clickhouse/rebuild/observe.ts +7 -3
- package/src/clickhouse/rebuild/op.ts +9 -0
- package/src/clickhouse/rebuild/rebuild.e2e.test.ts +62 -0
- package/src/clickhouse/rebuild/rebuild.test.ts +54 -0
- package/src/clickhouse/rebuild/steps.ts +136 -1
- package/src/clickhouse/testing/fake-server.ts +2 -0
- package/src/clickhouse/testing/writable-server.ts +2 -0
- package/src/clickhouse/topology.ts +17 -2
- package/src/clickhouse.ts +19 -0
- package/src/codegen/classifier-page.ts +2 -0
- package/src/codegen/registry.ts +10 -0
- package/src/config.ts +22 -3
- package/src/core/references.ts +5 -3
- package/src/core/statements.ts +11 -1
- package/src/files/build-root.ts +62 -0
- package/src/files/clickhouse.ts +52 -0
- package/src/files/common.ts +54 -0
- package/src/files/postgres.ts +371 -0
- package/src/generated/lexicon-sql.json +50 -0
- package/src/import-parser.ts +61 -0
- package/src/index.ts +14 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlch001.ts +26 -4
- package/src/lint/rules/sqlch002.ts +3 -2
- package/src/lint/rules/sqlpg001.ts +7 -4
- package/src/lint/rules/templates.ts +2 -2
- package/src/lsp/postgres-template.ts +2 -0
- package/src/lsp/template.ts +3 -3
- package/src/mcp/index.ts +6 -6
- package/src/mcp/postgres.ts +1 -1
- package/src/migration-statements.test.ts +25 -0
- package/src/migration-statements.ts +8 -0
- package/src/op/activities/clickhouse-rebuild.ts +1 -0
- package/src/op/activities/postgres-migration.ts +27 -2
- package/src/op/activity-contracts.ts +2 -0
- package/src/plugin.ts +29 -5
- 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 +117 -4
- package/src/postgres/apply/constraint-sequences.e2e.test.ts +179 -0
- package/src/postgres/apply/safe-sequences.e2e.test.ts +176 -0
- package/src/postgres/apply/statements.test.ts +138 -6
- package/src/postgres/apply/statements.ts +210 -18
- package/src/postgres/entities.ts +287 -11
- package/src/postgres/entity-types.ts +4 -0
- package/src/postgres/import/generator.ts +129 -46
- package/src/postgres/import/live-export.ts +12 -2
- package/src/postgres/live/bind.ts +20 -6
- package/src/postgres/live/catalog.ts +79 -3
- package/src/postgres/live/client.ts +10 -2
- package/src/postgres/live/describe-resources.ts +27 -3
- 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 +247 -8
- package/src/postgres/plan/commands.ts +60 -7
- package/src/postgres/plan/deep.ts +24 -3
- package/src/postgres/plan/diff.ts +71 -6
- package/src/postgres/plan/normalize.ts +115 -2
- package/src/postgres/plan/plan.test.ts +2 -2
- package/src/postgres/plan/rules.ts +49 -5
- package/src/postgres/plan/schema.ts +4 -2
- package/src/postgres/plan/server-normalize.ts +29 -1
- package/src/postgres.ts +11 -0
- package/src/serializer.ts +14 -9
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres-migration.md +3 -1
- package/src/skills/chant-sql-postgres-plan.md +3 -3
- package/src/skills/chant-sql-postgres.md +9 -2
- package/src/skills/chant-sql-rebuild.md +3 -2
- package/src/skills/chant-sql.md +7 -1
- package/src/sql-files.test.ts +188 -0
- package/src/sql-files.ts +218 -0
- package/src/token-source.e2e.test.ts +142 -0
- package/src/token-source.test.ts +119 -0
- package/src/token-source.ts +189 -0
- package/src/validate.ts +10 -0
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
* machine) is not a failure, runs no onFailure, and the next run resumes the
|
|
36
36
|
* backfill from its receipts; a step that fails runs onFailure, and the next
|
|
37
37
|
* run starts again from a new column.
|
|
38
|
+
*
|
|
39
|
+
* `gates: "outer"` (#3687) leaves out both approval gates and the Contract
|
|
40
|
+
* phase, for a caller that runs the Op under an approval of its own: the run
|
|
41
|
+
* goes from Plan to Retain, verification included. `onFailure: "keep"`
|
|
42
|
+
* leaves out the onFailure, so a failed run's next run resumes the backfill
|
|
43
|
+
* from its receipts instead of starting again.
|
|
38
44
|
*/
|
|
39
45
|
|
|
40
46
|
import { Op, activity, gate, phase, stepOutput, type GateApproval } from "@intentius/chant/op";
|
|
@@ -98,6 +104,27 @@ export interface PostgresMigrationOpConfig {
|
|
|
98
104
|
stack?: string;
|
|
99
105
|
/** Ownership env. Default: `ownership.env` in `chant.config.ts`. */
|
|
100
106
|
ownershipEnv?: string;
|
|
107
|
+
/**
|
|
108
|
+
* Whose approval the switch and the contract wait for. `"own"` (the
|
|
109
|
+
* default): the Op's two gates. `"outer"`: an approval the caller already
|
|
110
|
+
* holds for a larger change that runs this Op as one of its steps. The
|
|
111
|
+
* switch gate, the contract gate and the Contract phase are left out, so
|
|
112
|
+
* the Op ends at Retain with the old column kept until its retention date.
|
|
113
|
+
* Drop it after that date, or run the Op with `"own"`: its switch gate has
|
|
114
|
+
* nothing left to switch, and its contract gate binds that old column. The
|
|
115
|
+
* verification still runs, and any difference fails the run with nothing
|
|
116
|
+
* switched.
|
|
117
|
+
*/
|
|
118
|
+
gates?: "own" | "outer";
|
|
119
|
+
/**
|
|
120
|
+
* What a failed run does with what the expand added. `"drop"` (the
|
|
121
|
+
* default): onFailure drops the new column, the dual-write trigger and the
|
|
122
|
+
* receipts, and the next run starts again from a new column. `"keep"`:
|
|
123
|
+
* there is no onFailure, and the next run resumes the backfill from its
|
|
124
|
+
* per-batch receipts. To start again instead, run the Op once with
|
|
125
|
+
* `"drop"`.
|
|
126
|
+
*/
|
|
127
|
+
onFailure?: "drop" | "keep";
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
export interface PostgresMigrationOpResources {
|
|
@@ -119,6 +146,8 @@ export interface PostgresMigrationArgs {
|
|
|
119
146
|
stack?: string;
|
|
120
147
|
ownershipEnv?: string;
|
|
121
148
|
cwd?: string;
|
|
149
|
+
/** A failed run keeps what the expand added (`onFailure: "keep"`): a refusal says how to start again. */
|
|
150
|
+
keepOnFailure?: boolean;
|
|
122
151
|
}
|
|
123
152
|
|
|
124
153
|
const IDENT = /^[A-Za-z_][A-Za-z0-9_$]*$|^"(?:[^"]|"")+"$/;
|
|
@@ -134,6 +163,14 @@ export function PostgresMigrationOp(config: PostgresMigrationOpConfig): Postgres
|
|
|
134
163
|
}
|
|
135
164
|
if (config.replicationLag) for (const d of [config.replicationLag.max, config.replicationLag.wait]) if (d !== undefined) parseDuration(d);
|
|
136
165
|
if (config.retain !== undefined) parseDuration(config.retain);
|
|
166
|
+
if (config.gates !== undefined && config.gates !== "own" && config.gates !== "outer") {
|
|
167
|
+
throw new Error(`PostgresMigrationOp "${config.name}": gates must be "own" or "outer"`);
|
|
168
|
+
}
|
|
169
|
+
if (config.onFailure !== undefined && config.onFailure !== "drop" && config.onFailure !== "keep") {
|
|
170
|
+
throw new Error(`PostgresMigrationOp "${config.name}": onFailure must be "drop" or "keep"`);
|
|
171
|
+
}
|
|
172
|
+
const outer = config.gates === "outer";
|
|
173
|
+
const keep = config.onFailure === "keep";
|
|
137
174
|
|
|
138
175
|
const args: PostgresMigrationArgs = {
|
|
139
176
|
table: config.table,
|
|
@@ -149,6 +186,7 @@ export function PostgresMigrationOp(config: PostgresMigrationOpConfig): Postgres
|
|
|
149
186
|
...(config.stack ? { stack: config.stack } : {}),
|
|
150
187
|
...(config.ownershipEnv ? { ownershipEnv: config.ownershipEnv } : {}),
|
|
151
188
|
...(config.path && config.path !== "." ? { cwd: config.path } : {}),
|
|
189
|
+
...(keep ? { keepOnFailure: true } : {}),
|
|
152
190
|
};
|
|
153
191
|
const a = args as unknown as Record<string, unknown>;
|
|
154
192
|
const switchGate = config.gate?.gate ?? `approve-${config.name}`;
|
|
@@ -159,6 +197,33 @@ export function PostgresMigrationOp(config: PostgresMigrationOpConfig): Postgres
|
|
|
159
197
|
const step = (fn: string, extra: Partial<StepDefinition & { id: string; outcomeAttribute: unknown; profile: string; timeout: string }> = {}): StepDefinition =>
|
|
160
198
|
({ ...activity(fn, a), ...extra }) as StepDefinition;
|
|
161
199
|
|
|
200
|
+
// The switch waits for this gate, unless the caller holds the approval (`gates: "outer"`).
|
|
201
|
+
const approve = phase("Approve", [
|
|
202
|
+
gate(switchGate, {
|
|
203
|
+
...(config.gate?.timeout ? { timeout: config.gate.timeout } : {}),
|
|
204
|
+
plan: stepOutput("verify", "planDigest"),
|
|
205
|
+
description:
|
|
206
|
+
config.gate?.description ??
|
|
207
|
+
`Approve switching ${what} on ${config.env}: the run record's Verification line has what this approval binds; for a rename, readers move to the new name once it is switched`,
|
|
208
|
+
...(config.gate?.approval
|
|
209
|
+
? {
|
|
210
|
+
approval: {
|
|
211
|
+
...config.gate.approval,
|
|
212
|
+
...(policy
|
|
213
|
+
? {
|
|
214
|
+
context: {
|
|
215
|
+
verifiedRows: stepOutput("verify", "rows"),
|
|
216
|
+
mismatched: stepOutput("verify", "mismatched"),
|
|
217
|
+
...config.gate.approval.context,
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
: {}),
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
: {}),
|
|
224
|
+
}),
|
|
225
|
+
]);
|
|
226
|
+
|
|
162
227
|
const phases = [
|
|
163
228
|
...(config.build === false ? [] : [phase("Build", [activity("chantBuild", { path: config.path ?? "." })])]),
|
|
164
229
|
phase("Plan", [step("postgresMigrationPlan", { id: "plan", outcomeAttribute: [{ name: "MigrationState", from: "state" }, { name: "Change", from: "change" }] })]),
|
|
@@ -194,49 +259,29 @@ export function PostgresMigrationOp(config: PostgresMigrationOpConfig): Postgres
|
|
|
194
259
|
],
|
|
195
260
|
}),
|
|
196
261
|
]),
|
|
197
|
-
|
|
198
|
-
gate(switchGate, {
|
|
199
|
-
...(config.gate?.timeout ? { timeout: config.gate.timeout } : {}),
|
|
200
|
-
plan: stepOutput("verify", "planDigest"),
|
|
201
|
-
description:
|
|
202
|
-
config.gate?.description ??
|
|
203
|
-
`Approve switching ${what} on ${config.env}: the run record's Verification line has what this approval binds; for a rename, readers move to the new name once it is switched`,
|
|
204
|
-
...(config.gate?.approval
|
|
205
|
-
? {
|
|
206
|
-
approval: {
|
|
207
|
-
...config.gate.approval,
|
|
208
|
-
...(policy
|
|
209
|
-
? {
|
|
210
|
-
context: {
|
|
211
|
-
verifiedRows: stepOutput("verify", "rows"),
|
|
212
|
-
mismatched: stepOutput("verify", "mismatched"),
|
|
213
|
-
...config.gate.approval.context,
|
|
214
|
-
},
|
|
215
|
-
}
|
|
216
|
-
: {}),
|
|
217
|
-
},
|
|
218
|
-
}
|
|
219
|
-
: {}),
|
|
220
|
-
}),
|
|
221
|
-
]),
|
|
262
|
+
...(outer ? [] : [approve]),
|
|
222
263
|
phase("Switch", [step("postgresMigrationSwitch", { timeout: "6h", outcomeAttribute: [{ name: "OldColumn", from: "oldColumn" }] })]),
|
|
223
264
|
phase("Retain", [step("postgresMigrationRetain", { id: "retain", outcomeAttribute: { name: "RetainUntil", from: "retainUntil" } })]),
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
265
|
+
...(outer
|
|
266
|
+
? []
|
|
267
|
+
: [
|
|
268
|
+
phase("Approve contract", [
|
|
269
|
+
gate(contractGate, {
|
|
270
|
+
...(config.contractGate?.timeout ? { timeout: config.contractGate.timeout } : {}),
|
|
271
|
+
plan: stepOutput("retain", "contractDigest"),
|
|
272
|
+
description: config.contractGate?.description ?? `Approve dropping the old column of ${what} once its retention has passed and no reader uses it`,
|
|
273
|
+
}),
|
|
274
|
+
]),
|
|
275
|
+
phase("Contract", [step("postgresMigrationContract", { outcomeAttribute: { name: "Dropped", from: "dropped" } })]),
|
|
276
|
+
]),
|
|
232
277
|
];
|
|
233
278
|
|
|
234
279
|
const op = Op({
|
|
235
280
|
name: config.name,
|
|
236
|
-
overview: `Migrate ${what} on ${config.env} as expand and contract: new column, dual write, backfill, verify, gated switch, gated contract`,
|
|
281
|
+
overview: `Migrate ${what} on ${config.env} as expand and contract: new column, dual write, backfill, verify, ${outer ? "switch under the caller's approval" : "gated switch, gated contract"}`,
|
|
237
282
|
labels: { Migration: "true", Env: config.env, Table: config.table, Column: config.column },
|
|
238
283
|
phases,
|
|
239
|
-
onFailure: [phase("Compensate", [step("postgresMigrationCompensate")])],
|
|
284
|
+
...(keep ? {} : { onFailure: [phase("Compensate", [step("postgresMigrationCompensate")])] }),
|
|
240
285
|
});
|
|
241
286
|
return { op };
|
|
242
287
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PostgresMigrationOp` under an outer approval with what the expand added
|
|
3
|
+
* kept on failure (#3687): a backfill that fails part way leaves the new
|
|
4
|
+
* column, its dual write and its receipts; the next run skips the batches
|
|
5
|
+
* already filled, verifies and switches with no gate, and keeps the old
|
|
6
|
+
* column; a later run with the Op's own gates stops at its gates. A refusal
|
|
7
|
+
* says how to start again, since nothing drops what was kept.
|
|
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_3687`, 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 { OpRunFailure } from "@intentius/chant/op";
|
|
21
|
+
import { dockerAvailable, startTestPostgres, type TestPostgres } from "../testing/server";
|
|
22
|
+
import { connectPostgres, type PostgresClient, type PostgresEndpoint } from "../live/client";
|
|
23
|
+
import { postgresApply } from "../../op/activities/postgres-apply";
|
|
24
|
+
import * as m from "../../op/activities/postgres-migration";
|
|
25
|
+
import type { PostgresMigrationArgs, PostgresMigrationOpConfig } from "./op";
|
|
26
|
+
import { ApprovingLedger, runMigrationOp, runOutcome } from "../testing/migration";
|
|
27
|
+
import { POSTGRES_RECEIPTS_TABLE } from "./receipts";
|
|
28
|
+
|
|
29
|
+
const S = "chant_e2e_3687";
|
|
30
|
+
const given = process.env.CHANT_SQL_E2E_POSTGRES_URL;
|
|
31
|
+
const enabled = given ? true : await dockerAvailable();
|
|
32
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-pg-outer-"));
|
|
33
|
+
let server: TestPostgres | undefined;
|
|
34
|
+
let endpoint: PostgresEndpoint;
|
|
35
|
+
let admin: PostgresClient;
|
|
36
|
+
|
|
37
|
+
beforeAll(async () => {
|
|
38
|
+
if (!enabled) return;
|
|
39
|
+
if (given) endpoint = { url: given, password: process.env.CHANT_SQL_E2E_POSTGRES_PASSWORD ?? "" };
|
|
40
|
+
else {
|
|
41
|
+
server = await startTestPostgres();
|
|
42
|
+
endpoint = server.endpoint();
|
|
43
|
+
}
|
|
44
|
+
admin = await connectPostgres(endpoint);
|
|
45
|
+
await admin.query(`DROP SCHEMA IF EXISTS ${S} CASCADE`);
|
|
46
|
+
}, 600_000);
|
|
47
|
+
|
|
48
|
+
afterAll(async () => {
|
|
49
|
+
if (enabled) {
|
|
50
|
+
await admin?.query(`DROP SCHEMA IF EXISTS ${S} CASCADE`).catch(() => undefined);
|
|
51
|
+
await admin?.end();
|
|
52
|
+
}
|
|
53
|
+
await server?.stop();
|
|
54
|
+
rmSync(dir, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
interface Obj {
|
|
58
|
+
export: string;
|
|
59
|
+
type: string;
|
|
60
|
+
ddl: string;
|
|
61
|
+
dependsOn?: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeBuild(file: string, objects: Obj[]): string {
|
|
65
|
+
writeFileSync(join(dir, file), JSON.stringify({ dialect: "postgres", postgresMajor: 18, applyOrder: objects.map((o) => o.export), objects: objects.map((o) => ({ dependsOn: [], ...o })) }));
|
|
66
|
+
return file;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const MARKER = { stack: "e2e3687", env: "test" };
|
|
70
|
+
const profile = () => ({
|
|
71
|
+
config: { ownership: MARKER, sql: { profiles: { e2e: { url: endpoint.url, password: { env: "PG_E2E_PASSWORD" }, schemas: [S] } } } },
|
|
72
|
+
env: { PG_E2E_PASSWORD: endpoint.password ?? "" },
|
|
73
|
+
});
|
|
74
|
+
const deps = (extra: Partial<m.PostgresMigrationDeps> = {}): m.PostgresMigrationDeps => ({ ...profile(), log: () => undefined, ...extra });
|
|
75
|
+
const apply = (file: string) => postgresApply({ buildPath: join(dir, file), environment: "e2e" }, undefined, { ...profile(), log: () => undefined });
|
|
76
|
+
const one = async <T>(sql: string): Promise<T> => (await admin.query<T>(sql))[0]!;
|
|
77
|
+
const columnsOf = async (table: string) =>
|
|
78
|
+
(await admin.query<{ name: string }>(`SELECT attname AS name FROM pg_attribute WHERE attrelid = '${table}'::regclass AND attnum > 0 AND NOT attisdropped ORDER BY attnum`)).map((r) => r.name);
|
|
79
|
+
const receipts = async () => (await one<{ n: number }>(`SELECT count(*)::int AS n FROM ${S}.${POSTGRES_RECEIPTS_TABLE} WHERE address LIKE 'e2e3687/test/migrate/${S}.events.kind/%' AND address NOT LIKE '%/batches'`)).n;
|
|
80
|
+
|
|
81
|
+
describe.skipIf(!enabled)("under an outer approval, a failed migration keeps its new column and the rerun resumes (#3687)", () => {
|
|
82
|
+
const SCHEMA: Obj = { export: "app", type: "Postgres::Schema", ddl: `CREATE SCHEMA ${S}` };
|
|
83
|
+
const V1: Obj = { export: "events", type: "Postgres::Table", dependsOn: ["app"], ddl: `CREATE TABLE ${S}.events (id bigint PRIMARY KEY, kind varchar(20) NOT NULL)` };
|
|
84
|
+
const V2: Obj = { ...V1, ddl: `CREATE TABLE ${S}.events (id bigint PRIMARY KEY, kind varchar(10) NOT NULL)` };
|
|
85
|
+
const config = (): PostgresMigrationOpConfig => ({
|
|
86
|
+
name: "migrate-events-kind",
|
|
87
|
+
env: "e2e",
|
|
88
|
+
table: `${S}.events`,
|
|
89
|
+
column: "kind",
|
|
90
|
+
build: false,
|
|
91
|
+
path: dir,
|
|
92
|
+
output: "events-v2.json",
|
|
93
|
+
retain: "0s",
|
|
94
|
+
batchSize: 100,
|
|
95
|
+
stack: MARKER.stack,
|
|
96
|
+
ownershipEnv: MARKER.env,
|
|
97
|
+
gates: "outer",
|
|
98
|
+
onFailure: "keep",
|
|
99
|
+
});
|
|
100
|
+
const args = (): PostgresMigrationArgs => ({ table: `${S}.events`, column: "kind", buildPath: "events-v2.json", environment: "e2e", batchSize: 100, stack: MARKER.stack, ownershipEnv: MARKER.env, cwd: dir });
|
|
101
|
+
|
|
102
|
+
test("the failed run keeps the new column and its receipts; the rerun skips what was filled, switches without a gate and keeps the old column", async () => {
|
|
103
|
+
await apply(writeBuild("events-v1.json", [SCHEMA, V1]));
|
|
104
|
+
await admin.query(`INSERT INTO ${S}.events SELECT g, CASE WHEN g % 2 = 0 THEN 'click' ELSE 'view' END FROM generate_series(0, 599) g`);
|
|
105
|
+
writeBuild("events-v2.json", [SCHEMA, V2]);
|
|
106
|
+
|
|
107
|
+
// A refusal says how to start again when nothing drops what was kept; without keepOnFailure it does not.
|
|
108
|
+
await expect(m.postgresMigrationBackfill({ ...args(), keepOnFailure: true }, undefined, deps())).rejects.toThrow(
|
|
109
|
+
/the Expand phase adds it\. This migration keeps what the expand added on failure \(onFailure: "keep"\), so nothing drops it: to start again from a new column, run the Op once with onFailure: "drop"/,
|
|
110
|
+
);
|
|
111
|
+
await expect(m.postgresMigrationBackfill(args(), undefined, deps())).rejects.not.toThrow(/onFailure: "keep"/);
|
|
112
|
+
|
|
113
|
+
// Every attempt of the backfill step fails after its batch: three attempts, three batches filled, then the run fails.
|
|
114
|
+
let filled = 0;
|
|
115
|
+
const ledger = new ApprovingLedger();
|
|
116
|
+
const failure = await runMigrationOp(config(), ledger.port, () =>
|
|
117
|
+
deps({
|
|
118
|
+
backfill: {
|
|
119
|
+
afterBatch: () => {
|
|
120
|
+
filled++;
|
|
121
|
+
throw new Error(`the backfill failed after ${filled} batch(es)`);
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
).then(
|
|
126
|
+
() => undefined,
|
|
127
|
+
(e: unknown) => e,
|
|
128
|
+
);
|
|
129
|
+
expect(failure).toBeInstanceOf(OpRunFailure);
|
|
130
|
+
const records = (failure as OpRunFailure).result.records;
|
|
131
|
+
expect(records.find((x) => x.fn === "postgresMigrationBackfill")?.error).toMatch(/the backfill failed after 3 batch/);
|
|
132
|
+
// No onFailure ran: the new column, the dual write and the receipts are there.
|
|
133
|
+
expect(records.find((x) => x.fn === "postgresMigrationCompensate")).toBeUndefined();
|
|
134
|
+
expect(await columnsOf(`${S}.events`)).toEqual(["id", "kind", "kind__chant_new"]);
|
|
135
|
+
expect(await one(`SELECT count(*)::int AS n FROM pg_trigger WHERE tgrelid = '${S}.events'::regclass AND tgname = 'kind__chant_sync'`)).toEqual({ n: 1 });
|
|
136
|
+
expect(await one(`SELECT count(*)::int AS n FROM ${S}.events WHERE kind__chant_new IS NOT NULL`)).toEqual({ n: 300 });
|
|
137
|
+
expect(await receipts()).toBe(3);
|
|
138
|
+
|
|
139
|
+
// The rerun resumes: three batches skipped by receipt, three filled, verified and switched, with no gate.
|
|
140
|
+
const r = await runMigrationOp(config(), ledger.port, () => deps());
|
|
141
|
+
expect(r.status).toBe("ok");
|
|
142
|
+
expect(ledger.port.appended).toEqual([]);
|
|
143
|
+
expect(runOutcome(r, "Skipped")).toBe(3);
|
|
144
|
+
expect(runOutcome(r, "Filled")).toBe(3);
|
|
145
|
+
expect(runOutcome(r, "VerifiedRows")).toBe(600);
|
|
146
|
+
expect(r.records.map((x) => x.fn)).not.toContain("postgresMigrationContract");
|
|
147
|
+
expect(await one(`SELECT format_type(atttypid, atttypmod) AS t FROM pg_attribute WHERE attrelid = '${S}.events'::regclass AND attname = 'kind'`)).toEqual({ t: "character varying(10)" });
|
|
148
|
+
// The old column is kept: dropping it is a run with the Op's own gates.
|
|
149
|
+
expect(await columnsOf(`${S}.events`)).toEqual(["id", "kind__chant_old", "kind"]);
|
|
150
|
+
|
|
151
|
+
// Run with its own gates, the Op has nothing left to switch, and its contract gate binds that old column.
|
|
152
|
+
const own = new ApprovingLedger();
|
|
153
|
+
const first = await runMigrationOp({ ...config(), gates: "own" }, own.port, () => deps());
|
|
154
|
+
expect(first.status).toBe("gated");
|
|
155
|
+
expect(first.gate).toMatchObject({ gate: "approve-migrate-events-kind" });
|
|
156
|
+
expect(runOutcome(first, "MigrationState")).toBe("switched");
|
|
157
|
+
own.approveLast();
|
|
158
|
+
const second = await runMigrationOp({ ...config(), gates: "own" }, own.port, () => deps());
|
|
159
|
+
expect(second.status).toBe("gated");
|
|
160
|
+
expect(second.gate).toMatchObject({ gate: "approve-migrate-events-kind-contract" });
|
|
161
|
+
expect(await columnsOf(`${S}.events`)).toEqual(["id", "kind__chant_old", "kind"]);
|
|
162
|
+
}, 300_000);
|
|
163
|
+
});
|
package/src/postgres/parser.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* A hand-written, lossless parser for the Postgres statements the dialect
|
|
3
3
|
* declares (chant #3278, #3279): `CREATE SCHEMA`, `TABLE`, `INDEX`, `VIEW`,
|
|
4
4
|
* `MATERIALIZED VIEW`, `SEQUENCE`, `TYPE ... AS ENUM`, `DOMAIN`, `EXTENSION`,
|
|
5
|
-
* `FUNCTION`, `PROCEDURE` and `TRIGGER` (#3680), and `
|
|
6
|
-
*
|
|
5
|
+
* `FUNCTION`, `PROCEDURE` and `TRIGGER` (#3680), `POLICY` and `ROLE`, `GRANT`,
|
|
6
|
+
* `REVOKE` and `ALTER DEFAULT PRIVILEGES` on schemas, tables, columns,
|
|
7
|
+
* sequences and routines, `ALTER TABLE ... ROW LEVEL SECURITY` (#3681), and
|
|
8
|
+
* `COMMENT ON` for those objects.
|
|
7
9
|
*
|
|
8
10
|
* A function's or procedure's body is a string (`AS $$ ... $$`), kept
|
|
9
11
|
* verbatim: Postgres stores it as written and prints it back unchanged. A
|
|
@@ -260,6 +262,58 @@ export interface TriggerNode {
|
|
|
260
262
|
args: Span[];
|
|
261
263
|
}
|
|
262
264
|
|
|
265
|
+
export interface PolicyNode {
|
|
266
|
+
statement: "policy";
|
|
267
|
+
name: NameNode;
|
|
268
|
+
table: NameNode;
|
|
269
|
+
/** `AS PERMISSIVE` (true, the default) or `AS RESTRICTIVE` (false). */
|
|
270
|
+
permissive?: boolean;
|
|
271
|
+
command?: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
|
|
272
|
+
/** `TO role, ...`: role names, `public` for PUBLIC. */
|
|
273
|
+
roles: Array<{ name: string; span: Span }>;
|
|
274
|
+
/** `USING (...)`, parentheses excluded. */
|
|
275
|
+
using?: Span;
|
|
276
|
+
/** `WITH CHECK (...)`, parentheses excluded. */
|
|
277
|
+
check?: Span;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface RoleNode {
|
|
281
|
+
statement: "role";
|
|
282
|
+
name: NameNode;
|
|
283
|
+
/** The options, each by its key word (`LOGIN`, `NOINHERIT`, `CONNECTION LIMIT`, `VALID UNTIL`), with a value where it takes one. */
|
|
284
|
+
options: Array<{ option: string; value?: Span }>;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** What a GRANT or REVOKE is on: an object kind, or (for default privileges) a kind of object created later. */
|
|
288
|
+
export type GrantTarget = "TABLE" | "SEQUENCE" | "SCHEMA" | "FUNCTION" | "PROCEDURE" | "ROUTINE" | "TABLES" | "SEQUENCES" | "FUNCTIONS" | "ROUTINES" | "TYPES" | "SCHEMAS";
|
|
289
|
+
|
|
290
|
+
export interface GrantNode {
|
|
291
|
+
statement: "grant";
|
|
292
|
+
revoke: boolean;
|
|
293
|
+
/** `REVOKE GRANT OPTION FOR ...`. */
|
|
294
|
+
grantOptionFor: boolean;
|
|
295
|
+
/** Each privilege, upper case (`ALL` for ALL [PRIVILEGES]), with the columns it is limited to. */
|
|
296
|
+
privileges: Array<{ privilege: string; columns: Array<{ name: string; span: Span }> }>;
|
|
297
|
+
on: GrantTarget;
|
|
298
|
+
/** The objects, each with a routine's parameter list as written (parentheses excluded). Empty for default privileges. */
|
|
299
|
+
objects: Array<{ name: NameNode; args?: Span }>;
|
|
300
|
+
/** Role names, `public` for PUBLIC. */
|
|
301
|
+
grantees: Array<{ name: string; span: Span }>;
|
|
302
|
+
withGrantOption: boolean;
|
|
303
|
+
/** `ALTER DEFAULT PRIVILEGES FOR ROLE ...`. */
|
|
304
|
+
forRoles?: Array<{ name: string; span: Span }>;
|
|
305
|
+
/** `ALTER DEFAULT PRIVILEGES IN SCHEMA ...`. */
|
|
306
|
+
inSchemas?: Array<{ name: string; span: Span }>;
|
|
307
|
+
/** True for `ALTER DEFAULT PRIVILEGES`. */
|
|
308
|
+
defaults: boolean;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface RowSecurityNode {
|
|
312
|
+
statement: "rowSecurity";
|
|
313
|
+
table: NameNode;
|
|
314
|
+
action: "ENABLE" | "DISABLE" | "FORCE" | "NO FORCE";
|
|
315
|
+
}
|
|
316
|
+
|
|
263
317
|
export interface CommentNode {
|
|
264
318
|
statement: "comment";
|
|
265
319
|
/** TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION, FUNCTION, PROCEDURE, TRIGGER, CONSTRAINT, DOMAIN CONSTRAINT. */
|
|
@@ -284,6 +338,10 @@ export type StatementNode =
|
|
|
284
338
|
| ExtensionNode
|
|
285
339
|
| RoutineNode
|
|
286
340
|
| TriggerNode
|
|
341
|
+
| PolicyNode
|
|
342
|
+
| RoleNode
|
|
343
|
+
| GrantNode
|
|
344
|
+
| RowSecurityNode
|
|
287
345
|
| CommentNode;
|
|
288
346
|
|
|
289
347
|
const isNameToken = (t: Token | undefined): boolean =>
|
|
@@ -805,8 +863,15 @@ class PgParser extends SqlCursor {
|
|
|
805
863
|
|
|
806
864
|
parse(): StatementNode {
|
|
807
865
|
if (this.accept("COMMENT")) return this.comment();
|
|
866
|
+
if (this.accept("GRANT")) return this.grant(false, false);
|
|
867
|
+
if (this.accept("REVOKE")) return this.grant(true, false);
|
|
868
|
+
if (this.acceptSeq("ALTER", "DEFAULT", "PRIVILEGES")) return this.defaultPrivileges();
|
|
869
|
+
if (this.acceptSeq("ALTER", "TABLE")) return this.rowSecurity();
|
|
808
870
|
this.expect("CREATE");
|
|
809
871
|
const orReplace = this.acceptSeq("OR", "REPLACE");
|
|
872
|
+
if (!orReplace && this.accept("POLICY")) return this.policy();
|
|
873
|
+
if (!orReplace && this.accept("ROLE")) return this.role();
|
|
874
|
+
if (!orReplace && kw(this.peek(), "USER", "GROUP")) this.fail("expected ROLE (declare a role with CREATE ROLE; LOGIN is an option of it)");
|
|
810
875
|
if (this.accept("FUNCTION")) return this.routine("function", orReplace);
|
|
811
876
|
if (this.accept("PROCEDURE")) return this.routine("procedure", orReplace);
|
|
812
877
|
if (this.accept("TRIGGER")) return this.trigger(orReplace, false);
|
|
@@ -825,7 +890,7 @@ class PgParser extends SqlCursor {
|
|
|
825
890
|
const recursive = this.accept("RECURSIVE");
|
|
826
891
|
if (!orReplace && !recursive && persistence === undefined && this.acceptSeq("MATERIALIZED", "VIEW")) return this.view(true, false, false, false);
|
|
827
892
|
if (this.accept("VIEW")) return this.view(false, orReplace, persistence === "temporary", recursive);
|
|
828
|
-
return this.fail("expected SCHEMA, TABLE, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, EXTENSION, FUNCTION, PROCEDURE or
|
|
893
|
+
return this.fail("expected SCHEMA, TABLE, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, EXTENSION, FUNCTION, PROCEDURE, TRIGGER, POLICY or ROLE");
|
|
829
894
|
}
|
|
830
895
|
|
|
831
896
|
private schema(): SchemaNode {
|
|
@@ -1306,6 +1371,165 @@ class PgParser extends SqlCursor {
|
|
|
1306
1371
|
return node;
|
|
1307
1372
|
}
|
|
1308
1373
|
|
|
1374
|
+
/** A role name where one is granted to or named: an identifier, an interpolation, or PUBLIC (`public`). */
|
|
1375
|
+
private roleSpec(what = "a role name"): { name: string; span: Span } {
|
|
1376
|
+
const t = this.peek();
|
|
1377
|
+
if (kw(t, "CURRENT_USER", "CURRENT_ROLE", "SESSION_USER")) this.fail(`expected ${what} (name the role: ${t!.text.toUpperCase()} is whichever role runs the statement)`);
|
|
1378
|
+
if (kw(t, "PUBLIC")) {
|
|
1379
|
+
const i = this.idx();
|
|
1380
|
+
this.p++;
|
|
1381
|
+
return { name: "public", span: { from: i, to: i + 1, refs: [] } };
|
|
1382
|
+
}
|
|
1383
|
+
const n = this.next();
|
|
1384
|
+
if (!isNameToken(n)) this.fail(`expected ${what}`, n);
|
|
1385
|
+
const i = this.idx(-1);
|
|
1386
|
+
return { name: n.kind === "ref" ? "" : identValue(n), span: { from: i, to: i + 1, refs: n.kind === "ref" ? [n.part] : [] } };
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
private roleList(what?: string): Array<{ name: string; span: Span }> {
|
|
1390
|
+
const out = [this.roleSpec(what)];
|
|
1391
|
+
while (this.acceptPunct(",")) out.push(this.roleSpec(what));
|
|
1392
|
+
return out;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
private policy(): PolicyNode {
|
|
1396
|
+
const { text, span } = this.nameTok("a policy name");
|
|
1397
|
+
this.expect("ON");
|
|
1398
|
+
const node: PolicyNode = { statement: "policy", name: { pieces: [text], span }, table: this.qualifiedName("a table name"), roles: [] };
|
|
1399
|
+
if (this.accept("AS")) {
|
|
1400
|
+
if (this.accept("PERMISSIVE")) node.permissive = true;
|
|
1401
|
+
else {
|
|
1402
|
+
this.expect("RESTRICTIVE");
|
|
1403
|
+
node.permissive = false;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
if (this.accept("FOR")) {
|
|
1407
|
+
if (!this.accept("ALL", "SELECT", "INSERT", "UPDATE", "DELETE")) this.fail("expected ALL, SELECT, INSERT, UPDATE or DELETE");
|
|
1408
|
+
node.command = this.lastSeen()!.text.toUpperCase() as PolicyNode["command"];
|
|
1409
|
+
}
|
|
1410
|
+
if (this.accept("TO")) node.roles = this.roleList();
|
|
1411
|
+
if (this.accept("USING")) {
|
|
1412
|
+
if (!this.isPunct("(")) this.fail("expected '('");
|
|
1413
|
+
node.using = this.parenthesized();
|
|
1414
|
+
}
|
|
1415
|
+
if (this.acceptSeq("WITH", "CHECK")) {
|
|
1416
|
+
if (!this.isPunct("(")) this.fail("expected '('");
|
|
1417
|
+
node.check = this.parenthesized();
|
|
1418
|
+
}
|
|
1419
|
+
return node;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
private role(): RoleNode {
|
|
1423
|
+
const { text, span } = this.nameTok("a role name");
|
|
1424
|
+
const node: RoleNode = { statement: "role", name: { pieces: [text], span }, options: [] };
|
|
1425
|
+
this.accept("WITH");
|
|
1426
|
+
const FLAGS = ["SUPERUSER", "NOSUPERUSER", "CREATEDB", "NOCREATEDB", "CREATEROLE", "NOCREATEROLE", "INHERIT", "NOINHERIT", "LOGIN", "NOLOGIN", "REPLICATION", "NOREPLICATION", "BYPASSRLS", "NOBYPASSRLS"];
|
|
1427
|
+
for (;;) {
|
|
1428
|
+
if (this.atEnd()) return node;
|
|
1429
|
+
if (kw(this.peek(), ...FLAGS)) node.options.push({ option: this.next().text.toUpperCase() });
|
|
1430
|
+
else if (this.acceptSeq("CONNECTION", "LIMIT")) {
|
|
1431
|
+
const from = this.idx();
|
|
1432
|
+
if (this.isOp("-")) this.p++;
|
|
1433
|
+
const t = this.next();
|
|
1434
|
+
if (t.kind !== "number" && t.kind !== "ref") this.fail("expected a number", t);
|
|
1435
|
+
node.options.push({ option: "CONNECTION LIMIT", value: this.span(from, t.kind === "ref" ? [t.part] : []) });
|
|
1436
|
+
} else if (kw(this.peek(), "PASSWORD", "ENCRYPTED", "UNENCRYPTED") || this.is("VALID", "UNTIL")) {
|
|
1437
|
+
this.fail("expected a role option (a password is the environment's secret, never a declaration's: set it where the environment's credentials are kept)");
|
|
1438
|
+
} else if (kw(this.peek(), "IN", "ROLE", "ADMIN", "USER", "SYSID")) {
|
|
1439
|
+
this.fail("expected a role option (who is a member of which role is the environment's: grant memberships where the roles are provisioned)");
|
|
1440
|
+
} else return this.fail("expected a role option");
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/** `GRANT|REVOKE privileges ON target TO|FROM roles`, after GRANT or REVOKE; for default privileges, `on` is a plural kind and there are no objects. */
|
|
1445
|
+
private grant(revoke: boolean, defaults: boolean): GrantNode {
|
|
1446
|
+
const grantOptionFor = revoke && this.acceptSeq("GRANT", "OPTION", "FOR");
|
|
1447
|
+
const privileges: GrantNode["privileges"] = [];
|
|
1448
|
+
if (this.accept("ALL")) {
|
|
1449
|
+
this.accept("PRIVILEGES");
|
|
1450
|
+
const columns = this.isPunct("(") ? this.nameList() : [];
|
|
1451
|
+
privileges.push({ privilege: "ALL", columns });
|
|
1452
|
+
} else {
|
|
1453
|
+
const KNOWN = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER", "MAINTAIN", "USAGE", "CREATE", "EXECUTE"];
|
|
1454
|
+
do {
|
|
1455
|
+
const t = this.next();
|
|
1456
|
+
if (!kw(t, ...KNOWN)) {
|
|
1457
|
+
if (t.kind === "ident" || t.kind === "qident" || t.kind === "ref") {
|
|
1458
|
+
this.fail("expected a privilege (a role granted to a role is membership, which is the environment's: grant it where the roles are provisioned)", t);
|
|
1459
|
+
}
|
|
1460
|
+
this.fail("expected a privilege", t);
|
|
1461
|
+
}
|
|
1462
|
+
privileges.push({ privilege: t.text.toUpperCase(), columns: this.isPunct("(") ? this.nameList() : [] });
|
|
1463
|
+
} while (this.acceptPunct(","));
|
|
1464
|
+
}
|
|
1465
|
+
this.expect("ON");
|
|
1466
|
+
const node: GrantNode = { statement: "grant", revoke, grantOptionFor, privileges, on: "TABLE", objects: [], grantees: [], withGrantOption: false, defaults };
|
|
1467
|
+
if (defaults) {
|
|
1468
|
+
if (!this.accept("TABLES", "SEQUENCES", "FUNCTIONS", "ROUTINES", "TYPES", "SCHEMAS")) this.fail("expected TABLES, SEQUENCES, FUNCTIONS, ROUTINES, TYPES or SCHEMAS");
|
|
1469
|
+
node.on = this.lastSeen()!.text.toUpperCase() as GrantTarget;
|
|
1470
|
+
} else {
|
|
1471
|
+
if (this.accept("ALL")) {
|
|
1472
|
+
this.fail("expected an object (ALL ... IN SCHEMA grants on what exists when it runs; grant on each declared object, and use ALTER DEFAULT PRIVILEGES for what is created later)", this.lastSeen());
|
|
1473
|
+
}
|
|
1474
|
+
if (kw(this.peek(), "DATABASE", "DOMAIN", "TYPE", "LANGUAGE", "LARGE", "TABLESPACE", "FOREIGN", "PARAMETER")) {
|
|
1475
|
+
this.fail("expected TABLE, SEQUENCE, SCHEMA, FUNCTION, PROCEDURE or ROUTINE (privileges on this kind of object are not declared)");
|
|
1476
|
+
}
|
|
1477
|
+
if (this.accept("TABLE", "SEQUENCE", "SCHEMA", "FUNCTION", "PROCEDURE", "ROUTINE")) node.on = this.lastSeen()!.text.toUpperCase() as GrantTarget;
|
|
1478
|
+
const routine = node.on === "FUNCTION" || node.on === "PROCEDURE" || node.on === "ROUTINE";
|
|
1479
|
+
do {
|
|
1480
|
+
const name = this.qualifiedName(node.on === "SCHEMA" ? "a schema name" : "an object name");
|
|
1481
|
+
node.objects.push(routine && this.isPunct("(") ? { name, args: this.parenthesized() } : { name });
|
|
1482
|
+
} while (this.acceptPunct(","));
|
|
1483
|
+
}
|
|
1484
|
+
if (revoke) this.expect("FROM");
|
|
1485
|
+
else this.expect("TO");
|
|
1486
|
+
this.accept("GROUP");
|
|
1487
|
+
node.grantees = this.roleList();
|
|
1488
|
+
if (!revoke && this.acceptSeq("WITH", "GRANT", "OPTION")) node.withGrantOption = true;
|
|
1489
|
+
if (this.acceptSeq("GRANTED", "BY")) this.fail("expected the end of the statement (GRANTED BY names another grantor; chant grants as the role it runs as)", this.lastSeen());
|
|
1490
|
+
if (revoke && this.accept("CASCADE", "RESTRICT")) {
|
|
1491
|
+
if (this.lastSeen()!.text.toUpperCase() === "CASCADE") this.fail("expected the end of the statement (CASCADE revokes what others granted from it; revoke each grant)", this.lastSeen());
|
|
1492
|
+
}
|
|
1493
|
+
return node;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
private defaultPrivileges(): GrantNode {
|
|
1497
|
+
let forRoles: GrantNode["forRoles"];
|
|
1498
|
+
let inSchemas: GrantNode["inSchemas"];
|
|
1499
|
+
for (;;) {
|
|
1500
|
+
if (this.accept("FOR")) {
|
|
1501
|
+
this.expect("ROLE", "USER");
|
|
1502
|
+
forRoles = this.roleList();
|
|
1503
|
+
} else if (this.acceptSeq("IN", "SCHEMA")) {
|
|
1504
|
+
inSchemas = [];
|
|
1505
|
+
do inSchemas.push((({ text, span }) => ({ name: text, span }))(this.nameTok("a schema name")));
|
|
1506
|
+
while (this.acceptPunct(","));
|
|
1507
|
+
} else break;
|
|
1508
|
+
}
|
|
1509
|
+
let node: GrantNode;
|
|
1510
|
+
if (this.accept("GRANT")) node = this.grant(false, true);
|
|
1511
|
+
else {
|
|
1512
|
+
this.expect("REVOKE");
|
|
1513
|
+
node = this.grant(true, true);
|
|
1514
|
+
}
|
|
1515
|
+
return { ...node, ...(forRoles ? { forRoles } : {}), ...(inSchemas ? { inSchemas } : {}) };
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
private rowSecurity(): RowSecurityNode {
|
|
1519
|
+
this.accept("ONLY");
|
|
1520
|
+
const table = this.qualifiedName("a table name");
|
|
1521
|
+
let action: RowSecurityNode["action"];
|
|
1522
|
+
if (this.accept("ENABLE")) action = "ENABLE";
|
|
1523
|
+
else if (this.accept("DISABLE")) action = "DISABLE";
|
|
1524
|
+
else if (this.accept("FORCE")) action = "FORCE";
|
|
1525
|
+
else if (this.acceptSeq("NO", "FORCE")) action = "NO FORCE";
|
|
1526
|
+
else return this.fail("expected ENABLE, DISABLE, FORCE or NO FORCE ROW LEVEL SECURITY (a table's other changes are its CREATE TABLE's)");
|
|
1527
|
+
this.expect("ROW");
|
|
1528
|
+
this.expect("LEVEL");
|
|
1529
|
+
this.expect("SECURITY");
|
|
1530
|
+
return { statement: "rowSecurity", table, action };
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1309
1533
|
private comment(): CommentNode {
|
|
1310
1534
|
this.expect("ON");
|
|
1311
1535
|
let objectType: string;
|
|
@@ -1318,16 +1542,19 @@ class PgParser extends SqlCursor {
|
|
|
1318
1542
|
this.expect("ON");
|
|
1319
1543
|
if (this.accept("DOMAIN")) objectType = "DOMAIN CONSTRAINT";
|
|
1320
1544
|
on = this.qualifiedName("a table name");
|
|
1321
|
-
} else if (this.accept("TRIGGER")) {
|
|
1322
|
-
objectType =
|
|
1323
|
-
target = this.qualifiedName(
|
|
1545
|
+
} else if (this.accept("TRIGGER", "POLICY")) {
|
|
1546
|
+
objectType = this.lastSeen()!.text.toUpperCase();
|
|
1547
|
+
target = this.qualifiedName(`a ${objectType.toLowerCase()} name`);
|
|
1324
1548
|
this.expect("ON");
|
|
1325
1549
|
on = this.qualifiedName("a table name");
|
|
1550
|
+
} else if (this.accept("ROLE")) {
|
|
1551
|
+
objectType = "ROLE";
|
|
1552
|
+
target = this.qualifiedName("a role name");
|
|
1326
1553
|
} else {
|
|
1327
1554
|
if (this.acceptSeq("MATERIALIZED", "VIEW")) objectType = "MATERIALIZED VIEW";
|
|
1328
1555
|
else if (this.accept("TABLE", "COLUMN", "INDEX", "VIEW", "SEQUENCE", "TYPE", "DOMAIN", "SCHEMA", "EXTENSION", "FUNCTION", "PROCEDURE")) {
|
|
1329
1556
|
objectType = this.tokens[this.idx(-1)]!.text.toUpperCase();
|
|
1330
|
-
} else return this.fail("expected TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION, FUNCTION, PROCEDURE, TRIGGER or CONSTRAINT");
|
|
1557
|
+
} else return this.fail("expected TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION, FUNCTION, PROCEDURE, TRIGGER, POLICY, ROLE or CONSTRAINT");
|
|
1331
1558
|
target = this.qualifiedName("an object name");
|
|
1332
1559
|
if ((objectType === "FUNCTION" || objectType === "PROCEDURE") && this.isPunct("(")) args = this.parenthesized();
|
|
1333
1560
|
}
|
|
@@ -1357,7 +1584,19 @@ class PgParser extends SqlCursor {
|
|
|
1357
1584
|
}
|
|
1358
1585
|
|
|
1359
1586
|
const describe = (n: StatementNode): string =>
|
|
1360
|
-
n.statement === "comment"
|
|
1587
|
+
n.statement === "comment"
|
|
1588
|
+
? "COMMENT ON"
|
|
1589
|
+
: n.statement === "enum"
|
|
1590
|
+
? "type definition"
|
|
1591
|
+
: n.statement === "grant"
|
|
1592
|
+
? n.defaults
|
|
1593
|
+
? "ALTER DEFAULT PRIVILEGES"
|
|
1594
|
+
: n.revoke
|
|
1595
|
+
? "REVOKE"
|
|
1596
|
+
: "GRANT"
|
|
1597
|
+
: n.statement === "rowSecurity"
|
|
1598
|
+
? "ALTER TABLE"
|
|
1599
|
+
: `${n.statement} definition`;
|
|
1361
1600
|
|
|
1362
1601
|
/**
|
|
1363
1602
|
* Parse a template: one CREATE, optionally followed by COMMENT ON statements,
|