@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.
Files changed (259) hide show
  1. package/dist/clickhouse/access.d.ts +132 -0
  2. package/dist/clickhouse/access.d.ts.map +1 -0
  3. package/dist/clickhouse/apply/apply.d.ts.map +1 -1
  4. package/dist/clickhouse/apply/statements.d.ts +2 -0
  5. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  6. package/dist/clickhouse/entities.d.ts +171 -2
  7. package/dist/clickhouse/entities.d.ts.map +1 -1
  8. package/dist/clickhouse/http.d.ts +3 -0
  9. package/dist/clickhouse/http.d.ts.map +1 -1
  10. package/dist/clickhouse/import/generator.d.ts +28 -0
  11. package/dist/clickhouse/import/generator.d.ts.map +1 -1
  12. package/dist/clickhouse/import/ir.d.ts.map +1 -1
  13. package/dist/clickhouse/import/parser.d.ts +3 -3
  14. package/dist/clickhouse/import/parser.d.ts.map +1 -1
  15. package/dist/clickhouse/live/bind.d.ts.map +1 -1
  16. package/dist/clickhouse/live/catalog.d.ts +41 -6
  17. package/dist/clickhouse/live/catalog.d.ts.map +1 -1
  18. package/dist/clickhouse/live/describe-resources.d.ts +3 -2
  19. package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
  20. package/dist/clickhouse/parser.d.ts +54 -3
  21. package/dist/clickhouse/parser.d.ts.map +1 -1
  22. package/dist/clickhouse/plan/commands.d.ts +3 -1
  23. package/dist/clickhouse/plan/commands.d.ts.map +1 -1
  24. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  25. package/dist/clickhouse/plan/diff.d.ts +12 -0
  26. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  27. package/dist/clickhouse/plan/normalize.d.ts +61 -1
  28. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  29. package/dist/clickhouse/plan/report.d.ts.map +1 -1
  30. package/dist/clickhouse/plan/rules.d.ts +7 -0
  31. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  32. package/dist/clickhouse/plan/schema.d.ts +8 -4
  33. package/dist/clickhouse/plan/schema.d.ts.map +1 -1
  34. package/dist/clickhouse/rebuild/backfill.d.ts +3 -1
  35. package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
  36. package/dist/clickhouse/rebuild/observe.d.ts +7 -3
  37. package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
  38. package/dist/clickhouse/rebuild/op.d.ts +8 -0
  39. package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
  40. package/dist/clickhouse/rebuild/steps.d.ts +48 -0
  41. package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
  42. package/dist/clickhouse/testing/fake-server.d.ts.map +1 -1
  43. package/dist/clickhouse/testing/writable-server.d.ts.map +1 -1
  44. package/dist/clickhouse/topology.d.ts.map +1 -1
  45. package/dist/clickhouse.d.ts +1 -1
  46. package/dist/clickhouse.d.ts.map +1 -1
  47. package/dist/codegen/classifier-page.d.ts.map +1 -1
  48. package/dist/codegen/registry.d.ts.map +1 -1
  49. package/dist/config.d.ts +70 -7
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/core/references.d.ts +4 -2
  52. package/dist/core/references.d.ts.map +1 -1
  53. package/dist/core/statements.d.ts +11 -0
  54. package/dist/core/statements.d.ts.map +1 -1
  55. package/dist/files/build-root.d.ts +26 -0
  56. package/dist/files/build-root.d.ts.map +1 -0
  57. package/dist/files/clickhouse.d.ts +19 -0
  58. package/dist/files/clickhouse.d.ts.map +1 -0
  59. package/dist/files/common.d.ts +25 -0
  60. package/dist/files/common.d.ts.map +1 -0
  61. package/dist/files/postgres.d.ts +36 -0
  62. package/dist/files/postgres.d.ts.map +1 -0
  63. package/dist/import-parser.d.ts +25 -0
  64. package/dist/import-parser.d.ts.map +1 -0
  65. package/dist/index.d.ts +2 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/integrity.json +8 -8
  68. package/dist/lint/rules/sqlch001.d.ts +1 -1
  69. package/dist/lint/rules/sqlch001.d.ts.map +1 -1
  70. package/dist/lint/rules/sqlch002.d.ts.map +1 -1
  71. package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
  72. package/dist/lint/rules/templates.d.ts +1 -1
  73. package/dist/lint/rules/templates.d.ts.map +1 -1
  74. package/dist/lsp/postgres-template.d.ts.map +1 -1
  75. package/dist/manifest.json +1 -1
  76. package/dist/meta.json +50 -0
  77. package/dist/okf/index.md +11 -1
  78. package/dist/okf/rules/SQLCH001.md +2 -2
  79. package/dist/okf/types/Dictionary.md +9 -0
  80. package/dist/okf/types/Function.md +9 -0
  81. package/dist/okf/types/Grant.md +9 -0
  82. package/dist/okf/types/PostgresDefaultPrivileges.md +9 -0
  83. package/dist/okf/types/PostgresGrant.md +9 -0
  84. package/dist/okf/types/PostgresPolicy.md +9 -0
  85. package/dist/okf/types/PostgresRole.md +9 -0
  86. package/dist/okf/types/Role.md +9 -0
  87. package/dist/okf/types/RowPolicy.md +9 -0
  88. package/dist/okf/types/User.md +9 -0
  89. package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
  90. package/dist/op/activities/postgres-migration.d.ts.map +1 -1
  91. package/dist/op/activity-contracts.d.ts +19 -0
  92. package/dist/op/activity-contracts.d.ts.map +1 -1
  93. package/dist/plugin.d.ts.map +1 -1
  94. package/dist/postgres/access/acl.d.ts +119 -0
  95. package/dist/postgres/access/acl.d.ts.map +1 -0
  96. package/dist/postgres/access/import.d.ts +13 -0
  97. package/dist/postgres/access/import.d.ts.map +1 -0
  98. package/dist/postgres/access/live.d.ts +24 -0
  99. package/dist/postgres/access/live.d.ts.map +1 -0
  100. package/dist/postgres/apply/apply.d.ts.map +1 -1
  101. package/dist/postgres/apply/statements.d.ts +71 -0
  102. package/dist/postgres/apply/statements.d.ts.map +1 -1
  103. package/dist/postgres/entities.d.ts +106 -4
  104. package/dist/postgres/entities.d.ts.map +1 -1
  105. package/dist/postgres/entity-types.d.ts +4 -0
  106. package/dist/postgres/entity-types.d.ts.map +1 -1
  107. package/dist/postgres/import/generator.d.ts +37 -1
  108. package/dist/postgres/import/generator.d.ts.map +1 -1
  109. package/dist/postgres/import/live-export.d.ts +6 -0
  110. package/dist/postgres/import/live-export.d.ts.map +1 -1
  111. package/dist/postgres/live/bind.d.ts +2 -0
  112. package/dist/postgres/live/bind.d.ts.map +1 -1
  113. package/dist/postgres/live/catalog.d.ts +11 -2
  114. package/dist/postgres/live/catalog.d.ts.map +1 -1
  115. package/dist/postgres/live/client.d.ts +3 -0
  116. package/dist/postgres/live/client.d.ts.map +1 -1
  117. package/dist/postgres/live/describe-resources.d.ts +9 -0
  118. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  119. package/dist/postgres/migrate/op.d.ts +29 -0
  120. package/dist/postgres/migrate/op.d.ts.map +1 -1
  121. package/dist/postgres/parser.d.ts +76 -3
  122. package/dist/postgres/parser.d.ts.map +1 -1
  123. package/dist/postgres/plan/commands.d.ts +12 -0
  124. package/dist/postgres/plan/commands.d.ts.map +1 -1
  125. package/dist/postgres/plan/deep.d.ts.map +1 -1
  126. package/dist/postgres/plan/diff.d.ts +10 -1
  127. package/dist/postgres/plan/diff.d.ts.map +1 -1
  128. package/dist/postgres/plan/normalize.d.ts +8 -1
  129. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  130. package/dist/postgres/plan/rules.d.ts +8 -0
  131. package/dist/postgres/plan/rules.d.ts.map +1 -1
  132. package/dist/postgres/plan/schema.d.ts +2 -1
  133. package/dist/postgres/plan/schema.d.ts.map +1 -1
  134. package/dist/postgres/plan/server-normalize.d.ts +4 -1
  135. package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
  136. package/dist/postgres.d.ts +1 -1
  137. package/dist/postgres.d.ts.map +1 -1
  138. package/dist/rules/postgres-templates.ts +2 -2
  139. package/dist/rules/sqlch001.ts +26 -4
  140. package/dist/rules/sqlch002.ts +3 -2
  141. package/dist/rules/sqlpg001.ts +7 -4
  142. package/dist/rules/templates.ts +2 -2
  143. package/dist/serializer.d.ts +2 -1
  144. package/dist/serializer.d.ts.map +1 -1
  145. package/dist/sql-files.d.ts +79 -0
  146. package/dist/sql-files.d.ts.map +1 -0
  147. package/dist/token-source.d.ts +88 -0
  148. package/dist/token-source.d.ts.map +1 -0
  149. package/dist/validate.d.ts.map +1 -1
  150. package/package.json +2 -2
  151. package/src/clickhouse/access.test.ts +203 -0
  152. package/src/clickhouse/access.ts +595 -0
  153. package/src/clickhouse/apply/apply.ts +29 -20
  154. package/src/clickhouse/apply/statements.ts +105 -8
  155. package/src/clickhouse/dictionary.test.ts +177 -0
  156. package/src/clickhouse/entities.ts +356 -6
  157. package/src/clickhouse/function.test.ts +91 -0
  158. package/src/clickhouse/http.ts +11 -2
  159. package/src/clickhouse/import/generator.ts +81 -46
  160. package/src/clickhouse/import/import.test.ts +9 -3
  161. package/src/clickhouse/import/ir.ts +6 -2
  162. package/src/clickhouse/import/parser.ts +10 -18
  163. package/src/clickhouse/live/access.e2e.test.ts +218 -0
  164. package/src/clickhouse/live/bind.ts +17 -6
  165. package/src/clickhouse/live/catalog.ts +106 -10
  166. package/src/clickhouse/live/describe-resources.ts +23 -10
  167. package/src/clickhouse/live/dictionaries.e2e.test.ts +186 -0
  168. package/src/clickhouse/live/functions.e2e.test.ts +155 -0
  169. package/src/clickhouse/live/live.test.ts +2 -2
  170. package/src/clickhouse/parser.ts +136 -4
  171. package/src/clickhouse/plan/commands.ts +18 -7
  172. package/src/clickhouse/plan/deep.ts +71 -11
  173. package/src/clickhouse/plan/diff.ts +65 -2
  174. package/src/clickhouse/plan/normalize.ts +200 -6
  175. package/src/clickhouse/plan/report.ts +14 -3
  176. package/src/clickhouse/plan/rules.ts +44 -1
  177. package/src/clickhouse/plan/schema.ts +40 -17
  178. package/src/clickhouse/plan/server-format.ts +1 -1
  179. package/src/clickhouse/rebuild/backfill.ts +5 -25
  180. package/src/clickhouse/rebuild/observe.ts +7 -3
  181. package/src/clickhouse/rebuild/op.ts +9 -0
  182. package/src/clickhouse/rebuild/rebuild.e2e.test.ts +62 -0
  183. package/src/clickhouse/rebuild/rebuild.test.ts +54 -0
  184. package/src/clickhouse/rebuild/steps.ts +136 -1
  185. package/src/clickhouse/testing/fake-server.ts +2 -0
  186. package/src/clickhouse/testing/writable-server.ts +2 -0
  187. package/src/clickhouse/topology.ts +17 -2
  188. package/src/clickhouse.ts +19 -0
  189. package/src/codegen/classifier-page.ts +2 -0
  190. package/src/codegen/registry.ts +10 -0
  191. package/src/config.ts +22 -3
  192. package/src/core/references.ts +5 -3
  193. package/src/core/statements.ts +11 -1
  194. package/src/files/build-root.ts +62 -0
  195. package/src/files/clickhouse.ts +52 -0
  196. package/src/files/common.ts +54 -0
  197. package/src/files/postgres.ts +371 -0
  198. package/src/generated/lexicon-sql.json +50 -0
  199. package/src/import-parser.ts +61 -0
  200. package/src/index.ts +14 -1
  201. package/src/lint/rules/postgres-templates.ts +2 -2
  202. package/src/lint/rules/sqlch001.ts +26 -4
  203. package/src/lint/rules/sqlch002.ts +3 -2
  204. package/src/lint/rules/sqlpg001.ts +7 -4
  205. package/src/lint/rules/templates.ts +2 -2
  206. package/src/lsp/postgres-template.ts +2 -0
  207. package/src/lsp/template.ts +3 -3
  208. package/src/mcp/index.ts +6 -6
  209. package/src/mcp/postgres.ts +1 -1
  210. package/src/migration-statements.test.ts +25 -0
  211. package/src/migration-statements.ts +8 -0
  212. package/src/op/activities/clickhouse-rebuild.ts +1 -0
  213. package/src/op/activities/postgres-migration.ts +27 -2
  214. package/src/op/activity-contracts.ts +2 -0
  215. package/src/plugin.ts +29 -5
  216. package/src/postgres/access/access.e2e.test.ts +191 -0
  217. package/src/postgres/access/access.test.ts +147 -0
  218. package/src/postgres/access/acl.ts +400 -0
  219. package/src/postgres/access/import.ts +64 -0
  220. package/src/postgres/access/live.ts +123 -0
  221. package/src/postgres/apply/apply.ts +117 -4
  222. package/src/postgres/apply/constraint-sequences.e2e.test.ts +179 -0
  223. package/src/postgres/apply/safe-sequences.e2e.test.ts +176 -0
  224. package/src/postgres/apply/statements.test.ts +138 -6
  225. package/src/postgres/apply/statements.ts +210 -18
  226. package/src/postgres/entities.ts +287 -11
  227. package/src/postgres/entity-types.ts +4 -0
  228. package/src/postgres/import/generator.ts +129 -46
  229. package/src/postgres/import/live-export.ts +12 -2
  230. package/src/postgres/live/bind.ts +20 -6
  231. package/src/postgres/live/catalog.ts +79 -3
  232. package/src/postgres/live/client.ts +10 -2
  233. package/src/postgres/live/describe-resources.ts +27 -3
  234. package/src/postgres/migrate/migrate.test.ts +24 -1
  235. package/src/postgres/migrate/op.ts +80 -35
  236. package/src/postgres/migrate/outer.e2e.test.ts +163 -0
  237. package/src/postgres/parser.ts +247 -8
  238. package/src/postgres/plan/commands.ts +60 -7
  239. package/src/postgres/plan/deep.ts +24 -3
  240. package/src/postgres/plan/diff.ts +71 -6
  241. package/src/postgres/plan/normalize.ts +115 -2
  242. package/src/postgres/plan/plan.test.ts +2 -2
  243. package/src/postgres/plan/rules.ts +49 -5
  244. package/src/postgres/plan/schema.ts +4 -2
  245. package/src/postgres/plan/server-normalize.ts +29 -1
  246. package/src/postgres.ts +11 -0
  247. package/src/serializer.ts +14 -9
  248. package/src/skill-defs.ts +1 -1
  249. package/src/skills/chant-sql-postgres-migration.md +3 -1
  250. package/src/skills/chant-sql-postgres-plan.md +3 -3
  251. package/src/skills/chant-sql-postgres.md +9 -2
  252. package/src/skills/chant-sql-rebuild.md +3 -2
  253. package/src/skills/chant-sql.md +7 -1
  254. package/src/sql-files.test.ts +188 -0
  255. package/src/sql-files.ts +218 -0
  256. package/src/token-source.e2e.test.ts +142 -0
  257. package/src/token-source.test.ts +119 -0
  258. package/src/token-source.ts +189 -0
  259. 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
- phase("Approve", [
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
- phase("Approve contract", [
225
- gate(contractGate, {
226
- ...(config.contractGate?.timeout ? { timeout: config.contractGate.timeout } : {}),
227
- plan: stepOutput("retain", "contractDigest"),
228
- description: config.contractGate?.description ?? `Approve dropping the old column of ${what} once its retention has passed and no reader uses it`,
229
- }),
230
- ]),
231
- phase("Contract", [step("postgresMigrationContract", { outcomeAttribute: { name: "Dropped", from: "dropped" } })]),
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
+ });
@@ -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 `COMMENT ON` for those
6
- * objects.
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 TRIGGER");
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 = "TRIGGER";
1323
- target = this.qualifiedName("a trigger name");
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" ? "COMMENT ON" : n.statement === "enum" ? "type definition" : `${n.statement} definition`;
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,