@intentius/chant-lexicon-sql 0.124.31 → 0.124.32

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 (322) hide show
  1. package/dist/change-set.js +250 -0
  2. package/dist/clickhouse/access.js +722 -0
  3. package/dist/clickhouse/apply/apply.js +243 -0
  4. package/dist/clickhouse/apply/mutations.js +66 -0
  5. package/dist/clickhouse/apply/statements.js +545 -0
  6. package/dist/clickhouse/catalog-types.js +7 -0
  7. package/dist/clickhouse/container.js +152 -0
  8. package/dist/clickhouse/engine-syntax.js +159 -0
  9. package/dist/clickhouse/entities.js +593 -0
  10. package/dist/clickhouse/expressions.js +107 -0
  11. package/dist/clickhouse/http.js +55 -0
  12. package/dist/clickhouse/import/generator.js +192 -0
  13. package/dist/clickhouse/import/inlined.js +207 -0
  14. package/dist/clickhouse/import/ir.js +160 -0
  15. package/dist/clickhouse/import/live-export.js +131 -0
  16. package/dist/clickhouse/import/parser.js +17 -0
  17. package/dist/clickhouse/live/bind.js +116 -0
  18. package/dist/clickhouse/live/catalog.js +223 -0
  19. package/dist/clickhouse/live/describe-resources.js +91 -0
  20. package/dist/clickhouse/live/undeclarable.js +59 -0
  21. package/dist/clickhouse/overlays/codecs.js +57 -0
  22. package/dist/clickhouse/overlays/engines.js +101 -0
  23. package/dist/clickhouse/overlays/kinds.js +1 -0
  24. package/dist/clickhouse/overlays/projections.js +26 -0
  25. package/dist/clickhouse/overlays/skip-indexes.js +40 -0
  26. package/dist/clickhouse/overlays/ttl.js +34 -0
  27. package/dist/clickhouse/overlays/types.js +59 -0
  28. package/dist/clickhouse/ownership.js +83 -0
  29. package/dist/clickhouse/parser.js +674 -0
  30. package/dist/clickhouse/plan/commands.js +159 -0
  31. package/dist/clickhouse/plan/deep.js +345 -0
  32. package/dist/clickhouse/plan/diff.js +312 -0
  33. package/dist/clickhouse/plan/disruption.js +73 -0
  34. package/dist/clickhouse/plan/normalize.js +545 -0
  35. package/dist/clickhouse/plan/rebuild-handoff.js +57 -0
  36. package/dist/clickhouse/plan/report.js +26 -0
  37. package/dist/clickhouse/plan/rules.js +72 -0
  38. package/dist/clickhouse/plan/schema.js +67 -0
  39. package/dist/clickhouse/plan/server-format.js +35 -0
  40. package/dist/clickhouse/rebuild/backfill.js +265 -0
  41. package/dist/clickhouse/rebuild/engines.js +146 -0
  42. package/dist/clickhouse/rebuild/observe.js +260 -0
  43. package/dist/clickhouse/rebuild/op.js +152 -0
  44. package/dist/clickhouse/rebuild/partitions.js +65 -0
  45. package/dist/clickhouse/rebuild/receipts.js +120 -0
  46. package/dist/clickhouse/rebuild/replicas.js +121 -0
  47. package/dist/clickhouse/rebuild/shards.js +76 -0
  48. package/dist/clickhouse/rebuild/steps.js +383 -0
  49. package/dist/clickhouse/rebuild/verify.js +279 -0
  50. package/dist/clickhouse/row-types.js +151 -0
  51. package/dist/clickhouse/testing/fake-server.js +47 -0
  52. package/dist/clickhouse/testing/roundtrip-schema.js +25 -0
  53. package/dist/clickhouse/testing/writable-server.js +123 -0
  54. package/dist/clickhouse/tokens.js +23 -0
  55. package/dist/clickhouse/topology.js +463 -0
  56. package/dist/clickhouse/type-syntax.js +73 -0
  57. package/dist/clickhouse.js +25 -0
  58. package/dist/codegen/classifier-page.js +93 -0
  59. package/dist/codegen/clickhouse-module.js +249 -0
  60. package/dist/codegen/coverage-page.js +70 -0
  61. package/dist/codegen/docs-cli.js +3 -0
  62. package/dist/codegen/docs.js +222 -0
  63. package/dist/codegen/generate-cli.js +13 -0
  64. package/dist/codegen/generate.js +97 -0
  65. package/dist/codegen/package.js +28 -0
  66. package/dist/codegen/postgres-classifier-page.js +95 -0
  67. package/dist/codegen/postgres-module.js +418 -0
  68. package/dist/codegen/registry.js +52 -0
  69. package/dist/composites/audit-log-table.js +35 -0
  70. package/dist/composites/catalog.js +524 -0
  71. package/dist/composites/cdc-mirror.js +32 -0
  72. package/dist/composites/clickhouse.js +6 -0
  73. package/dist/composites/events-table.js +26 -0
  74. package/dist/composites/index.js +8 -0
  75. package/dist/composites/join-table.js +25 -0
  76. package/dist/composites/postgres.js +6 -0
  77. package/dist/composites/refreshed-view.js +24 -0
  78. package/dist/composites/replacing-table.js +28 -0
  79. package/dist/composites/rollup-view.js +29 -0
  80. package/dist/composites/sharded-table.js +27 -0
  81. package/dist/composites/soft-delete-table.js +34 -0
  82. package/dist/composites/tenant-table.js +26 -0
  83. package/dist/composites/typed.js +8 -0
  84. package/dist/config.js +206 -0
  85. package/dist/core/apply.js +93 -0
  86. package/dist/core/classifier.js +115 -0
  87. package/dist/core/container.js +69 -0
  88. package/dist/core/cursor.js +261 -0
  89. package/dist/core/diff.js +48 -0
  90. package/dist/core/entity.js +58 -0
  91. package/dist/core/find-templates.js +118 -0
  92. package/dist/core/foreign-tables.js +27 -0
  93. package/dist/core/handoff.js +32 -0
  94. package/dist/core/index.js +64 -0
  95. package/dist/core/interpolation.js +191 -0
  96. package/dist/core/normalize.js +43 -0
  97. package/dist/core/ownership.js +119 -0
  98. package/dist/core/receipts.js +22 -0
  99. package/dist/core/references.js +61 -0
  100. package/dist/core/statements.js +42 -0
  101. package/dist/core/template.js +79 -0
  102. package/dist/core/testing/http-stub.js +21 -0
  103. package/dist/core/tokens.js +202 -0
  104. package/dist/core/typed.js +40 -0
  105. package/dist/core/undeclarable.js +46 -0
  106. package/dist/coverage.js +161 -0
  107. package/dist/detect.js +32 -0
  108. package/dist/dialects.js +18 -0
  109. package/dist/files/build-root.js +72 -0
  110. package/dist/files/clickhouse.js +138 -0
  111. package/dist/files/common.js +58 -0
  112. package/dist/files/postgres.js +389 -0
  113. package/dist/generated/clickhouse-types.js +5 -0
  114. package/dist/generated/clickhouse.js +4361 -0
  115. package/dist/generated/postgres-types.js +227 -0
  116. package/dist/generated/postgres.js +3667 -0
  117. package/dist/import-generator.js +14 -0
  118. package/dist/import-parser.js +71 -0
  119. package/dist/index.js +24 -0
  120. package/dist/init-templates.js +290 -0
  121. package/dist/integrity.json +2 -2
  122. package/dist/lint/audit-catalog.js +31 -0
  123. package/dist/lint/audit-lineage.js +22 -0
  124. package/dist/lint/post-synth/audit-entries.js +34 -0
  125. package/dist/lint/post-synth/clickhouse-helpers.js +79 -0
  126. package/dist/lint/post-synth/clickhouse-type-check.js +162 -0
  127. package/dist/lint/post-synth/index.js +112 -0
  128. package/dist/lint/post-synth/postgres-audit-entries.js +35 -0
  129. package/dist/lint/post-synth/postgres-helpers.js +65 -0
  130. package/dist/lint/post-synth/postgres-names.js +507 -0
  131. package/dist/lint/post-synth/sql-helpers.js +23 -0
  132. package/dist/lint/post-synth/sql101.js +59 -0
  133. package/dist/lint/post-synth/sql102.js +34 -0
  134. package/dist/lint/post-synth/sqlch101.js +34 -0
  135. package/dist/lint/post-synth/sqlch102.js +23 -0
  136. package/dist/lint/post-synth/sqlch103.js +45 -0
  137. package/dist/lint/post-synth/sqlch104.js +41 -0
  138. package/dist/lint/post-synth/sqlch105.js +36 -0
  139. package/dist/lint/post-synth/sqlch106.js +27 -0
  140. package/dist/lint/post-synth/sqlch107.js +39 -0
  141. package/dist/lint/post-synth/sqlch108.js +28 -0
  142. package/dist/lint/post-synth/sqlch109.js +21 -0
  143. package/dist/lint/post-synth/sqlch110.js +31 -0
  144. package/dist/lint/post-synth/sqlch111.js +41 -0
  145. package/dist/lint/post-synth/sqlch112.js +28 -0
  146. package/dist/lint/post-synth/sqlch113.js +19 -0
  147. package/dist/lint/post-synth/sqlch114.js +26 -0
  148. package/dist/lint/post-synth/sqlch115.js +30 -0
  149. package/dist/lint/post-synth/sqlch116.js +22 -0
  150. package/dist/lint/post-synth/sqlch117.js +26 -0
  151. package/dist/lint/post-synth/sqlch118.js +27 -0
  152. package/dist/lint/post-synth/sqlch119.js +32 -0
  153. package/dist/lint/post-synth/sqlch120.js +20 -0
  154. package/dist/lint/post-synth/sqlch121.js +11 -0
  155. package/dist/lint/post-synth/sqlch122.js +11 -0
  156. package/dist/lint/post-synth/sqlch123.js +10 -0
  157. package/dist/lint/post-synth/sqlch124.js +21 -0
  158. package/dist/lint/post-synth/sqlch125.js +55 -0
  159. package/dist/lint/post-synth/sqlch126.js +62 -0
  160. package/dist/lint/post-synth/sqlch127.js +132 -0
  161. package/dist/lint/post-synth/sqlpg101.js +25 -0
  162. package/dist/lint/post-synth/sqlpg102.js +35 -0
  163. package/dist/lint/post-synth/sqlpg103.js +23 -0
  164. package/dist/lint/post-synth/sqlpg104.js +23 -0
  165. package/dist/lint/post-synth/sqlpg105.js +22 -0
  166. package/dist/lint/post-synth/sqlpg106.js +22 -0
  167. package/dist/lint/post-synth/sqlpg107.js +22 -0
  168. package/dist/lint/post-synth/sqlpg108.js +28 -0
  169. package/dist/lint/post-synth/sqlpg109.js +39 -0
  170. package/dist/lint/post-synth/sqlpg110.js +38 -0
  171. package/dist/lint/post-synth/sqlpg111.js +23 -0
  172. package/dist/lint/post-synth/sqlpg112.js +25 -0
  173. package/dist/lint/post-synth/sqlpg113.js +29 -0
  174. package/dist/lint/post-synth/sqlpg114.js +39 -0
  175. package/dist/lint/post-synth/sqlpg115.js +48 -0
  176. package/dist/lint/post-synth/sqlpg116.js +26 -0
  177. package/dist/lint/post-synth/sqlpg117.js +24 -0
  178. package/dist/lint/post-synth/sqlpg118.js +22 -0
  179. package/dist/lint/post-synth/sqlpg119.js +41 -0
  180. package/dist/lint/post-synth/sqlpg120.js +83 -0
  181. package/dist/lint/post-synth/sqlpg121.js +33 -0
  182. package/dist/lint/post-synth/sqlpg122.js +27 -0
  183. package/dist/lint/post-synth/sqlpg123.js +69 -0
  184. package/dist/lint/post-synth/sqlpg124.js +50 -0
  185. package/dist/lint/post-synth/sqlpg125.js +41 -0
  186. package/dist/lint/post-synth/sqlpg126.js +38 -0
  187. package/dist/lint/rules/index.js +12 -0
  188. package/dist/lint/rules/postgres-templates.js +27 -0
  189. package/dist/lint/rules/sqlch001.js +79 -0
  190. package/dist/lint/rules/sqlch002.js +124 -0
  191. package/dist/lint/rules/sqlch003.js +69 -0
  192. package/dist/lint/rules/sqlch004.js +83 -0
  193. package/dist/lint/rules/sqlpg001.js +68 -0
  194. package/dist/lint/rules/sqlpg002.js +69 -0
  195. package/dist/lint/rules/sqlpg003.js +58 -0
  196. package/dist/lint/rules/sqlpg004.js +77 -0
  197. package/dist/lint/rules/sqlpg005.js +86 -0
  198. package/dist/lint/rules/templates.js +25 -0
  199. package/dist/lint/rules/typed-names.js +132 -0
  200. package/dist/live-dialect.js +32 -0
  201. package/dist/lsp/catalog.js +44 -0
  202. package/dist/lsp/completions.js +136 -0
  203. package/dist/lsp/hover.js +115 -0
  204. package/dist/lsp/postgres-catalog.js +176 -0
  205. package/dist/lsp/postgres-template.js +171 -0
  206. package/dist/lsp/postgres.js +266 -0
  207. package/dist/lsp/refs.js +60 -0
  208. package/dist/lsp/registry.d.ts.map +1 -1
  209. package/dist/lsp/registry.js +19 -0
  210. package/dist/lsp/template.js +143 -0
  211. package/dist/manifest.json +1 -1
  212. package/dist/mcp/index.d.ts.map +1 -1
  213. package/dist/mcp/index.js +293 -0
  214. package/dist/mcp/postgres.js +117 -0
  215. package/dist/migration-statements.js +243 -0
  216. package/dist/op/activities/clickhouse-apply.js +83 -0
  217. package/dist/op/activities/clickhouse-emulator.js +53 -0
  218. package/dist/op/activities/clickhouse-rebuild.js +119 -0
  219. package/dist/op/activities/index.js +26 -0
  220. package/dist/op/activities/postgres-apply.js +126 -0
  221. package/dist/op/activities/postgres-emulator.js +70 -0
  222. package/dist/op/activities/postgres-migration.js +156 -0
  223. package/dist/op/activities/sql-exec.js +57 -0
  224. package/dist/op/activity-contracts.js +103 -0
  225. package/dist/package-cli.js +14 -0
  226. package/dist/plugin.js +259 -0
  227. package/dist/postgres/access/acl.js +344 -0
  228. package/dist/postgres/access/changes.js +30 -0
  229. package/dist/postgres/access/import.js +61 -0
  230. package/dist/postgres/access/live.js +96 -0
  231. package/dist/postgres/apply/apply.js +557 -0
  232. package/dist/postgres/apply/statements.js +882 -0
  233. package/dist/postgres/catalog-types.js +18 -0
  234. package/dist/postgres/container.js +58 -0
  235. package/dist/postgres/entities.js +1390 -0
  236. package/dist/postgres/entity-types.js +28 -0
  237. package/dist/postgres/import/generator.js +283 -0
  238. package/dist/postgres/import/ir.js +53 -0
  239. package/dist/postgres/import/live-export.js +83 -0
  240. package/dist/postgres/keywords.js +32 -0
  241. package/dist/postgres/live/bind.js +130 -0
  242. package/dist/postgres/live/catalog-kinds.js +301 -0
  243. package/dist/postgres/live/catalog.js +644 -0
  244. package/dist/postgres/live/client.js +151 -0
  245. package/dist/postgres/live/describe-resources.js +135 -0
  246. package/dist/postgres/live/undeclarable.js +111 -0
  247. package/dist/postgres/migrate/backfill.js +170 -0
  248. package/dist/postgres/migrate/batches.js +122 -0
  249. package/dist/postgres/migrate/carry.js +518 -0
  250. package/dist/postgres/migrate/handoff.js +59 -0
  251. package/dist/postgres/migrate/names.js +61 -0
  252. package/dist/postgres/migrate/observe.js +360 -0
  253. package/dist/postgres/migrate/op.js +178 -0
  254. package/dist/postgres/migrate/receipts.js +117 -0
  255. package/dist/postgres/migrate/replication.js +106 -0
  256. package/dist/postgres/migrate/steps.js +514 -0
  257. package/dist/postgres/overlays/columns.js +31 -0
  258. package/dist/postgres/overlays/constraints.js +52 -0
  259. package/dist/postgres/overlays/extension-objects.js +88 -0
  260. package/dist/postgres/overlays/indexes.js +25 -0
  261. package/dist/postgres/overlays/kinds.js +2 -0
  262. package/dist/postgres/overlays/partitioning.js +23 -0
  263. package/dist/postgres/overlays/relations.js +25 -0
  264. package/dist/postgres/overlays/storage.js +65 -0
  265. package/dist/postgres/overlays/type-ddl.js +36 -0
  266. package/dist/postgres/overlays/types.js +74 -0
  267. package/dist/postgres/parser.js +1824 -0
  268. package/dist/postgres/plan/commands.js +137 -0
  269. package/dist/postgres/plan/deep.js +324 -0
  270. package/dist/postgres/plan/diff.js +499 -0
  271. package/dist/postgres/plan/disruption.js +64 -0
  272. package/dist/postgres/plan/normalize.js +889 -0
  273. package/dist/postgres/plan/report.js +21 -0
  274. package/dist/postgres/plan/rules.js +115 -0
  275. package/dist/postgres/plan/schema.js +105 -0
  276. package/dist/postgres/plan/server-normalize.js +166 -0
  277. package/dist/postgres/providers/aurora.js +44 -0
  278. package/dist/postgres/providers/azure.js +53 -0
  279. package/dist/postgres/providers/cloud-sql.js +41 -0
  280. package/dist/postgres/providers/index.js +73 -0
  281. package/dist/postgres/providers/neon.js +44 -0
  282. package/dist/postgres/providers/rds.js +47 -0
  283. package/dist/postgres/providers/supabase.js +45 -0
  284. package/dist/postgres/providers/types.js +21 -0
  285. package/dist/postgres/row-types.js +107 -0
  286. package/dist/postgres/testing/fake-client.js +34 -0
  287. package/dist/postgres/testing/migration.js +48 -0
  288. package/dist/postgres/testing/roundtrip-schema.js +64 -0
  289. package/dist/postgres/testing/server.js +47 -0
  290. package/dist/postgres/testing/writable-server.js +160 -0
  291. package/dist/postgres/tokens.js +41 -0
  292. package/dist/postgres.js +15 -0
  293. package/dist/read/clickhouse.js +82 -0
  294. package/dist/read/objects.js +44 -0
  295. package/dist/read/postgres.js +104 -0
  296. package/dist/read.js +52 -0
  297. package/dist/receipts.js +207 -0
  298. package/dist/serializer.js +172 -0
  299. package/dist/skill-defs.js +93 -0
  300. package/dist/spec/catalog.js +191 -0
  301. package/dist/spec/fetch.d.ts.map +1 -1
  302. package/dist/spec/fetch.js +90 -0
  303. package/dist/spec/pin.js +42 -0
  304. package/dist/spec/postgres-catalog.js +171 -0
  305. package/dist/spec/postgres-fetch.js +91 -0
  306. package/dist/spec/postgres-pin.js +82 -0
  307. package/dist/sql-files.js +206 -0
  308. package/dist/sql-types-command.js +97 -0
  309. package/dist/sql-types.js +323 -0
  310. package/dist/token-source.js +156 -0
  311. package/dist/validate-cli.js +5 -0
  312. package/dist/validate.js +151 -0
  313. package/dist/workspace-outputs.js +191 -0
  314. package/package.json +7 -7
  315. package/src/codegen/package.ts +2 -2
  316. package/src/lint/rules/typescript-7.test.ts +2 -1
  317. package/src/lsp/catalog.ts +2 -2
  318. package/src/lsp/postgres-catalog.ts +2 -2
  319. package/src/lsp/registry.ts +2 -1
  320. package/src/mcp/index.ts +4 -2
  321. package/src/spec/fetch.ts +2 -2
  322. package/src/spec/postgres-fetch.ts +2 -2
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The Postgres wire client: node-postgres (`pg`), loaded the first time a
3
+ * Postgres server is read, so a ClickHouse-only project never loads it.
4
+ *
5
+ * Every session reads the catalog with an empty `search_path`, as `pg_dump`
6
+ * does: `pg_get_*def()` and `format_type()` then qualify every name outside
7
+ * `pg_catalog`, so what a server prints does not depend on the role's own
8
+ * search path.
9
+ */
10
+ /** A failed statement, with the server's SQLSTATE when it sent one. */
11
+ export class PostgresQueryError extends Error {
12
+ constructor(message, code, position) {
13
+ super(message);
14
+ this.code = code;
15
+ this.position = position;
16
+ this.name = "PostgresQueryError";
17
+ }
18
+ }
19
+ let loaded;
20
+ async function pgModule() {
21
+ loaded ?? (loaded = import("pg").then((m) => (m.default ?? m)));
22
+ return loaded;
23
+ }
24
+ const wrap = (err) => {
25
+ const e = err;
26
+ return new PostgresQueryError(e.message ?? String(err), e.code, e.position !== undefined ? Number(e.position) : undefined);
27
+ };
28
+ /**
29
+ * The URL with libpq's meaning of `sslmode`. pg-connection-string reads
30
+ * `sslmode=require` as verify-full unless `uselibpqcompat=true` is set, so an
31
+ * RDS host without its CA bundle, or a tunnel whose host name doesn't match
32
+ * the certificate, fails to connect. In psql, `require` encrypts without
33
+ * verifying, and a profile url means what it means there. A url that sets
34
+ * `uselibpqcompat` itself is left alone.
35
+ */
36
+ export function withLibpqSslMode(url) {
37
+ if (url.searchParams.has("sslmode") && !url.searchParams.has("uselibpqcompat"))
38
+ url.searchParams.set("uselibpqcompat", "true");
39
+ return url;
40
+ }
41
+ /** Connect to `endpoint` and set the catalog-reading session up. */
42
+ export async function connectPostgres(endpoint, options = {}) {
43
+ const pg = await pgModule();
44
+ // node-postgres lets the connection string's fields win over the config's, so the credentials go into the URL.
45
+ const url = withLibpqSslMode(new URL(endpoint.url));
46
+ if (endpoint.user !== undefined)
47
+ url.username = encodeURIComponent(endpoint.user);
48
+ const password = endpoint.token ? await endpoint.token.get() : endpoint.password;
49
+ if (password !== undefined)
50
+ url.password = encodeURIComponent(password);
51
+ const client = new pg.Client({ connectionString: url.toString(), application_name: options.applicationName ?? "chant" });
52
+ // A server that goes away emits an error event; the next query fails with it instead of the process crashing.
53
+ client.on("error", () => undefined);
54
+ try {
55
+ await client.connect();
56
+ await client.query("SELECT pg_catalog.set_config('search_path', '', false)");
57
+ }
58
+ catch (err) {
59
+ await client.end().catch(() => undefined);
60
+ const wrapped = wrap(err);
61
+ // A refused token is not reused: the next connect mints another.
62
+ if (endpoint.token && (wrapped.code === "28P01" || wrapped.code === "28000"))
63
+ endpoint.token.invalidate();
64
+ throw wrapped;
65
+ }
66
+ let ended = false;
67
+ return {
68
+ async query(sql, params) {
69
+ try {
70
+ const r = await client.query(sql, params);
71
+ return r.rows;
72
+ }
73
+ catch (err) {
74
+ throw wrap(err);
75
+ }
76
+ },
77
+ async end() {
78
+ if (ended)
79
+ return;
80
+ ended = true;
81
+ await client.end().catch(() => undefined);
82
+ },
83
+ };
84
+ }
85
+ /** SQLSTATE lock_not_available: `lock_timeout` passed while a statement waited for a lock. */
86
+ export const SQLSTATE_LOCK_NOT_AVAILABLE = "55P03";
87
+ /** The `lock_timeout` a statement waits under when the profile sets none (`sql.profiles.<env>.lockTimeoutMs`): the applier's and the catalog reader's. */
88
+ export const DEFAULT_LOCK_TIMEOUT_MS = 5000;
89
+ /**
90
+ * The sessions holding ACCESS EXCLUSIVE on a relation in the current database,
91
+ * other than this one. Only that mode conflicts with the ACCESS SHARE a
92
+ * catalog function (`pg_get_viewdef()`, `pg_get_indexdef()`) takes. Empty when
93
+ * the lookup itself fails.
94
+ */
95
+ export async function accessExclusiveHolders(client) {
96
+ try {
97
+ const rows = await client.query(`SELECT l.pid, l.relation::pg_catalog.regclass::text AS relation, a.application_name AS application, a.state,
98
+ (EXTRACT(EPOCH FROM pg_catalog.now() - a.xact_start))::int AS seconds, pg_catalog.left(a.query, 120) AS query
99
+ FROM pg_catalog.pg_locks l LEFT JOIN pg_catalog.pg_stat_activity a ON a.pid = l.pid
100
+ WHERE l.locktype = 'relation' AND l.granted AND l.mode = 'AccessExclusiveLock'
101
+ AND l.pid <> pg_catalog.pg_backend_pid()
102
+ AND l.database = (SELECT oid FROM pg_catalog.pg_database WHERE datname = pg_catalog.current_database())
103
+ ORDER BY l.pid, 2 LIMIT 10`);
104
+ return rows.map((r) => ({
105
+ pid: Number(r.pid),
106
+ relation: r.relation,
107
+ ...(r.application ? { application: r.application } : {}),
108
+ ...(r.state ? { state: r.state } : {}),
109
+ ...(r.seconds !== null && r.seconds !== undefined ? { seconds: Number(r.seconds) } : {}),
110
+ ...(r.query ? { query: r.query.replace(/\s+/g, " ").trim() } : {}),
111
+ }));
112
+ }
113
+ catch {
114
+ return [];
115
+ }
116
+ }
117
+ /** One line naming the lock holders, for a message. */
118
+ export function describeLockHolders(holders) {
119
+ if (holders.length === 0)
120
+ return "no session holds ACCESS EXCLUSIVE now; the lock was released after the wait";
121
+ return holders
122
+ .map((h) => {
123
+ const about = [h.application ? `application ${JSON.stringify(h.application)}` : "", h.state ?? "", h.seconds !== undefined ? `transaction open ${h.seconds}s` : "", h.query ? `last statement: ${h.query}` : ""].filter(Boolean);
124
+ return `${h.relation} is held in ACCESS EXCLUSIVE by pid ${h.pid}${about.length > 0 ? ` (${about.join(", ")})` : ""}`;
125
+ })
126
+ .join("; ");
127
+ }
128
+ /**
129
+ * A client for catalog reads: the session's `lock_timeout` is `lockTimeoutMs`
130
+ * (0 waits without limit, as Postgres does), so a read that needs a table
131
+ * another session holds in ACCESS EXCLUSIVE (a long `ALTER TABLE`, `LOCK
132
+ * TABLE`) fails after the wait instead of hanging. The failure names the
133
+ * relation and the pid holding it.
134
+ */
135
+ export async function withCatalogLockTimeout(client, lockTimeoutMs = DEFAULT_LOCK_TIMEOUT_MS) {
136
+ await client.query("SELECT pg_catalog.set_config('lock_timeout', $1, false)", [`${lockTimeoutMs}ms`]);
137
+ return {
138
+ async query(sql, params) {
139
+ try {
140
+ return await client.query(sql, params);
141
+ }
142
+ catch (err) {
143
+ if (!(err instanceof PostgresQueryError) || err.code !== SQLSTATE_LOCK_NOT_AVAILABLE)
144
+ throw err;
145
+ const holders = describeLockHolders(await accessExclusiveHolders(client));
146
+ throw new PostgresQueryError(`${err.message.split("\n")[0]}: the catalog read waited ${lockTimeoutMs}ms for a lock; ${holders}. Wait for that transaction to finish, or raise sql.profiles.<env>.lockTimeoutMs`, err.code, err.position);
147
+ }
148
+ },
149
+ end: () => client.end(),
150
+ };
151
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `describeResources()` for the Postgres dialect: which declared objects exist
3
+ * on the environment's server.
4
+ *
5
+ * One catalog read per run (`./catalog.ts`), then a lookup per declared
6
+ * entity, so N entities are a fixed handful of queries. An object the catalog
7
+ * does not list is absent, and the `queried` address names the server and the
8
+ * object asked for. A server that cannot be reached, or refuses the
9
+ * credentials, leaves every entity unobserved with that reason: never absent.
10
+ *
11
+ * Ownership is chant's marker, the trailer on the object's comment
12
+ * (`../../core/ownership.ts`): `owned` when the comment carries it, `foreign`
13
+ * when it does not. With `owned: true` a foreign object is withheld as
14
+ * `filtered`. An object another tool keeps (an ORM's revision table) is
15
+ * foreign whatever its comment says.
16
+ */
17
+ import { observeEntities, } from "@intentius/chant/observation";
18
+ import { bindPostgres, classifyPostgresFailure, redactUrl } from "./bind.js";
19
+ import { liveKey, markProviderOwned, readLiveSchema } from "./catalog.js";
20
+ import { POSTGRES_ENTITY_TYPES } from "../entity-types.js";
21
+ import { isChantManaged, readMarker, stripMarker } from "../../core/ownership.js";
22
+ import { identValue } from "../parser.js";
23
+ import { canonicalPgObject } from "../plan/normalize.js";
24
+ /**
25
+ * The schema and name a declared entity is created as; a bare name is in the
26
+ * default schema. A routine's parameter types and a trigger's table tell it
27
+ * from another of the same name (`signature`).
28
+ */
29
+ export function declaredAddress(entity, defaultSchema) {
30
+ const name = String(entity.props.name ?? "");
31
+ if (entity.type === POSTGRES_ENTITY_TYPES.schema || entity.type === POSTGRES_ENTITY_TYPES.extension || entity.type === POSTGRES_ENTITY_TYPES.role || entity.type === POSTGRES_ENTITY_TYPES.eventTrigger)
32
+ return { name };
33
+ // A cast is named by its two types, as the catalog prints them.
34
+ if (entity.type === POSTGRES_ENTITY_TYPES.cast)
35
+ return { name: canonicalPgObject(entity.type, entity.props, defaultSchema).name };
36
+ const schema = typeof entity.props.schema === "string" ? entity.props.schema : identValue(defaultSchema);
37
+ if (entity.type === POSTGRES_ENTITY_TYPES.function ||
38
+ entity.type === POSTGRES_ENTITY_TYPES.procedure ||
39
+ entity.type === POSTGRES_ENTITY_TYPES.aggregate ||
40
+ entity.type === POSTGRES_ENTITY_TYPES.trigger ||
41
+ entity.type === POSTGRES_ENTITY_TYPES.policy) {
42
+ const signature = canonicalPgObject(entity.type, entity.props, defaultSchema).signature;
43
+ return { schema, name, ...(signature !== undefined ? { signature } : {}) };
44
+ }
45
+ return { schema, name };
46
+ }
47
+ /**
48
+ * Why an access declaration is not observed one by one: a grant or default
49
+ * privileges, which `chant sql plan` compares as the access they add up to;
50
+ * a policy or role where the profile does not manage access.
51
+ */
52
+ export function accessUnobserved(type, managed) {
53
+ if (type === POSTGRES_ENTITY_TYPES.grant || type === POSTGRES_ENTITY_TYPES.defaultPrivileges) {
54
+ return { reason: "unsupported-kind", detail: "a grant is compared as the privileges the declarations add up to, by chant sql plan and the applier, not one statement at a time" };
55
+ }
56
+ if (!managed && (type === POSTGRES_ENTITY_TYPES.policy || type === POSTGRES_ENTITY_TYPES.role)) {
57
+ return { reason: "filtered", detail: "access is not managed in this environment (sql.profiles.<env>.access)" };
58
+ }
59
+ return undefined;
60
+ }
61
+ /** A schema scope for the catalog read: the profile's schemas, else every schema the declarations name and the default one. */
62
+ export function scopeFor(target, entities) {
63
+ if (target.schemas)
64
+ return target.schemas;
65
+ if (entities.length === 0)
66
+ return undefined;
67
+ const out = new Set([target.defaultSchema]);
68
+ // The schema alone: the rest of an address (a routine's parameter types) needs props a caller may not have.
69
+ const unplaced = new Set([POSTGRES_ENTITY_TYPES.extension, POSTGRES_ENTITY_TYPES.role, POSTGRES_ENTITY_TYPES.cast, POSTGRES_ENTITY_TYPES.eventTrigger]);
70
+ for (const e of entities) {
71
+ if (e.type === POSTGRES_ENTITY_TYPES.schema)
72
+ out.add(String(e.props.name ?? ""));
73
+ else if (!unplaced.has(e.type))
74
+ out.add(typeof e.props.schema === "string" ? e.props.schema : identValue(target.defaultSchema));
75
+ }
76
+ return [...out].sort();
77
+ }
78
+ function adapter(options) {
79
+ return {
80
+ async bind() {
81
+ const { target, client } = await bindPostgres(options);
82
+ try {
83
+ const live = markProviderOwned(await readLiveSchema(client, {
84
+ schemas: scopeFor(target, options.declared.filter((d) => d.type.startsWith("Postgres::"))),
85
+ access: target.access === true,
86
+ roles: options.declared.filter((d) => d.type === POSTGRES_ENTITY_TYPES.role).map((d) => String(d.props.name)),
87
+ }), target.provider);
88
+ return { target, byKey: new Map(live.map((o) => [liveKey(o.type, o.schema, o.name, o.signature), o])) };
89
+ }
90
+ finally {
91
+ await client.end();
92
+ }
93
+ },
94
+ classifyBindFailure: (err) => classifyPostgresFailure(err),
95
+ async read({ target, byKey }, entity) {
96
+ if (!entity.type.startsWith("Postgres::"))
97
+ return { unobserved: { reason: "unsupported-kind", detail: entity.type } };
98
+ const why = accessUnobserved(entity.type, target.access === true);
99
+ if (why)
100
+ return { unobserved: why };
101
+ const { schema, name, signature } = declaredAddress(entity, target.defaultSchema);
102
+ const queried = `${redactUrl(target.endpoint.url)} ${schema ? `${schema}.` : ""}${name}${signature ?? ""}`;
103
+ const live = byKey.get(liveKey(entity.type, schema, name, signature));
104
+ if (!live)
105
+ return { absent: true, queried };
106
+ const owned = !live.foreign && isChantManaged(live.comment);
107
+ if (options.owned && !owned) {
108
+ return {
109
+ unobserved: { reason: "filtered", detail: live.foreign ? `${live.foreign} keeps this object` : "the object's comment carries no chant ownership marker and owned was requested" },
110
+ queried,
111
+ };
112
+ }
113
+ const marker = readMarker(live.comment);
114
+ const comment = live.comment ? stripMarker(live.comment) : "";
115
+ return {
116
+ present: {
117
+ type: live.type,
118
+ physicalId: live.oid,
119
+ status: "PRESENT",
120
+ attributes: { oid: live.oid, ...(comment ? { comment } : {}), ...(live.foreign ? { keptBy: live.foreign } : {}) },
121
+ ownership: owned ? "owned" : "foreign",
122
+ ...(owned && marker?.stack ? { marker: { stack: marker.stack, ...(marker.env ? { env: marker.env } : {}) } } : {}),
123
+ },
124
+ queried,
125
+ };
126
+ },
127
+ };
128
+ }
129
+ export async function describeResources(options) {
130
+ const declared = options.entityNames.map((name) => {
131
+ const entity = options.entities.get(name);
132
+ return { name, type: entity?.entityType ?? "", props: entity?.props ?? {} };
133
+ });
134
+ return observeEntities(declared, adapter({ ...options, declared }));
135
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The objects in a live Postgres database of the kinds no declaration holds
3
+ * (#3911), found so import can refuse them by name instead of leaving them
4
+ * out (`../../core/undeclarable.ts`).
5
+ *
6
+ * One catalog query per kind the coverage table marks refused
7
+ * (`../../coverage/postgres.json`), keyed by the row's `kind`. A kind whose
8
+ * row is flipped to declared is no longer asked about: its objects are then
9
+ * the catalog reader's (`./catalog.ts`) to read.
10
+ *
11
+ * Left out, as the reader leaves them out: the server's own schemas, objects
12
+ * an extension owns (`pg_depend.deptype = 'e'`; the extension is declared),
13
+ * the objects `initdb` makes, whose oids are below 16384
14
+ * (`FirstNormalObjectId`), and an object that is part of another
15
+ * (`deptype = 'i'`: the casts a range type makes for itself). An object in a
16
+ * schema is looked for in the schemas in scope; one that belongs to the whole
17
+ * database (a cast, an event trigger, a publication, a foreign data wrapper)
18
+ * is always looked for.
19
+ */
20
+ import { notExtensionOwned, scopeSql } from "./catalog.js";
21
+ import { KIND_COVERAGE } from "../../coverage.js";
22
+ /** `FirstNormalObjectId`: every oid below it is the server's own. */
23
+ const FIRST_USER_OID = 16384;
24
+ const C = 'COLLATE "C"';
25
+ /** Not part of another object (`pg_depend.deptype = 'i'`): the multirange cast a range type makes for itself is the range type's. */
26
+ const notInternal = (catalog, oid) => `NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = 'pg_catalog.${catalog}'::pg_catalog.regclass AND d.objid = ${oid} AND d.deptype = 'i')`;
27
+ const ns = (alias, column) => `JOIN pg_catalog.pg_namespace ${alias} ON ${alias}.oid = ${column}`;
28
+ const args = (oid) => `'(' || pg_catalog.replace(pg_catalog.oidvectortypes(${oid}), ', ', ',') || ')'`;
29
+ /** A probe for objects of a catalog in a schema, the catalog aliased `x`; `name` is the name expression. */
30
+ const inSchema = (catalog, nsColumn, name, where = "TRUE") => ({
31
+ sql: (scope) => `SELECT n.nspname AS schema, ${name} AS name, NULL::text AS owner FROM pg_catalog.${catalog} x ${ns("n", `x.${nsColumn}`)}
32
+ WHERE ${where} AND ${scope} AND x.oid >= ${FIRST_USER_OID} AND ${notExtensionOwned(catalog, "x.oid")} AND ${notInternal(catalog, "x.oid")}`,
33
+ });
34
+ /** A probe for objects that belong to the database, not a schema. */
35
+ const databaseWide = (catalog, name, owner) => ({
36
+ sql: () => `SELECT NULL::text AS schema, ${name} AS name, ${owner ? `pg_catalog.pg_get_userbyid(x.${owner})` : "NULL::text"} AS owner FROM pg_catalog.${catalog} x
37
+ WHERE x.oid >= ${FIRST_USER_OID} AND ${notExtensionOwned(catalog, "x.oid")} AND ${notInternal(catalog, "x.oid")}`,
38
+ });
39
+ /** The pg_type rows of one `typtype`. */
40
+ const type = (where) => inSchema("pg_type", "typnamespace", "x.typname", where);
41
+ export const POSTGRES_PROBES = {
42
+ "access method": databaseWide("pg_am", "x.amname"),
43
+ aggregate: inSchema("pg_proc", "pronamespace", `x.proname || ${args("x.proargtypes")}`, "x.prokind = 'a'"),
44
+ "routine with a SQL-standard body": inSchema("pg_proc", "pronamespace", `x.proname || ${args("x.proargtypes")}`, "x.prokind IN ('f', 'p', 'w') AND x.prosqlbody IS NOT NULL"),
45
+ cast: databaseWide("pg_cast", "'(' || pg_catalog.format_type(x.castsource, NULL) || ' AS ' || pg_catalog.format_type(x.casttarget, NULL) || ')'"),
46
+ collation: inSchema("pg_collation", "collnamespace", "x.collname"),
47
+ conversion: inSchema("pg_conversion", "connamespace", "x.conname"),
48
+ "event trigger": databaseWide("pg_event_trigger", "x.evtname", "evtowner"),
49
+ "foreign data wrapper": databaseWide("pg_foreign_data_wrapper", "x.fdwname", "fdwowner"),
50
+ "foreign server": databaseWide("pg_foreign_server", "x.srvname", "srvowner"),
51
+ "user mapping": {
52
+ // pg_user_mapping is the superuser's to read; the view shows every mapping without its options.
53
+ sql: () => `SELECT NULL::text AS schema, 'for ' || x.usename || ' on server ' || x.srvname AS name, pg_catalog.pg_get_userbyid(s.srvowner) AS owner FROM pg_catalog.pg_user_mappings x
54
+ JOIN pg_catalog.pg_foreign_server s ON s.oid = x.srvid
55
+ WHERE ${notExtensionOwned("pg_foreign_server", "s.oid")}`,
56
+ },
57
+ "foreign table": inSchema("pg_class", "relnamespace", "x.relname", "x.relkind = 'f'"),
58
+ "procedural language": databaseWide("pg_language", "x.lanname", "lanowner"),
59
+ operator: inSchema("pg_operator", "oprnamespace", "x.oprname || '(' || pg_catalog.format_type(x.oprleft, NULL) || ',' || pg_catalog.format_type(x.oprright, NULL) || ')'"),
60
+ "operator class": inSchema("pg_opclass", "opcnamespace", "x.opcname || ' USING ' || (SELECT a.amname FROM pg_catalog.pg_am a WHERE a.oid = x.opcmethod)"),
61
+ // A family CREATE OPERATOR CLASS made for itself, named as its class, is the class's.
62
+ "operator family": inSchema("pg_opfamily", "opfnamespace", "x.opfname || ' USING ' || (SELECT a.amname FROM pg_catalog.pg_am a WHERE a.oid = x.opfmethod)", "NOT EXISTS (SELECT 1 FROM pg_catalog.pg_opclass c WHERE c.opcfamily = x.oid AND c.opcname = x.opfname AND c.opcnamespace = x.opfnamespace)"),
63
+ publication: databaseWide("pg_publication", "x.pubname", "pubowner"),
64
+ subscription: {
65
+ sql: () => `SELECT NULL::text AS schema, x.subname AS name, pg_catalog.pg_get_userbyid(x.subowner) AS owner FROM pg_catalog.pg_subscription x
66
+ WHERE x.subdbid = (SELECT d.oid FROM pg_catalog.pg_database d WHERE d.datname = pg_catalog.current_database())`,
67
+ },
68
+ rule: {
69
+ sql: (scope) => `SELECT n.nspname AS schema, r.rulename || ' ON ' || c.relname AS name, NULL::text AS owner
70
+ FROM pg_catalog.pg_rewrite r JOIN pg_catalog.pg_class c ON c.oid = r.ev_class ${ns("n", "c.relnamespace")}
71
+ WHERE r.rulename <> '_RETURN' AND ${scope} AND ${notExtensionOwned("pg_class", "c.oid")}`,
72
+ },
73
+ "extended statistics": inSchema("pg_statistic_ext", "stxnamespace", "x.stxname"),
74
+ "text search configuration": inSchema("pg_ts_config", "cfgnamespace", "x.cfgname"),
75
+ "text search dictionary": inSchema("pg_ts_dict", "dictnamespace", "x.dictname"),
76
+ "text search parser": inSchema("pg_ts_parser", "prsnamespace", "x.prsname"),
77
+ "text search template": inSchema("pg_ts_template", "tmplnamespace", "x.tmplname"),
78
+ transform: databaseWide("pg_transform", "'FOR ' || pg_catalog.format_type(x.trftype, NULL) || ' LANGUAGE ' || (SELECT l.lanname FROM pg_catalog.pg_language l WHERE l.oid = x.trflang)"),
79
+ "composite type": type("x.typtype = 'c' AND EXISTS (SELECT 1 FROM pg_catalog.pg_class c WHERE c.oid = x.typrelid AND c.relkind = 'c')"),
80
+ "range type": type("x.typtype = 'r'"),
81
+ // A base type, or a shell; not the array type the server makes beside every type.
82
+ "base type": type("x.typtype IN ('b', 'p') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_type e WHERE e.typarray = x.oid)"),
83
+ };
84
+ /**
85
+ * Every object in scope of a kind the coverage table refuses, kind by kind
86
+ * in the table's order, each by schema then name. A kind the session may not
87
+ * read (a role without the privilege) is a problem in `unread`, not a pass.
88
+ */
89
+ export async function readUndeclarable(client, scope = {}) {
90
+ const s = scopeSql("n.nspname", scope);
91
+ const objects = [];
92
+ const unread = [];
93
+ for (const row of KIND_COVERAGE.postgres) {
94
+ if (row.status !== "refused")
95
+ continue;
96
+ const probe = POSTGRES_PROBES[row.kind];
97
+ if (!probe)
98
+ throw new Error(`no catalog query finds a Postgres ${row.kind} (src/postgres/live/undeclarable.ts)`);
99
+ // The comment names the kind, in the server's log and to a test's fake client.
100
+ const sql = `/* chant undeclarable: ${row.kind} */ SELECT q.schema, q.name, q.owner FROM (${probe.sql(s.sql)}) q ORDER BY q.schema ${C} NULLS FIRST, q.name ${C}`;
101
+ try {
102
+ const rows = await client.query(sql, sql.includes("$1") ? s.params : []);
103
+ for (const r of rows)
104
+ objects.push({ kind: row.kind, ...(r.schema ? { schema: r.schema } : {}), name: r.name, ...(r.owner ? { owner: r.owner } : {}) });
105
+ }
106
+ catch (err) {
107
+ unread.push(`${row.kind}: ${err instanceof Error ? err.message : String(err)}`);
108
+ }
109
+ }
110
+ return { objects, unread };
111
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * The backfill and the verification of the expand-and-contract migration
3
+ * (#3281).
4
+ *
5
+ * ## Backfill
6
+ *
7
+ * The rows that were in the table before the dual write started are filled
8
+ * in batches: one range of the primary key per batch (`./batches.ts`:
9
+ * `[b * size, (b + 1) * size)` of an integer key, else between boundaries
10
+ * recorded in the receipt store), so a batch is the same rows on every run
11
+ * however many rows were written since. Each batch is one transaction:
12
+ *
13
+ * UPDATE t SET new = <expression> WHERE key >= lo AND key < hi AND new IS DISTINCT FROM <expression>;
14
+ * INSERT INTO __chant_receipts ... -- the batch's receipt, last
15
+ * COMMIT;
16
+ *
17
+ * under `lock_timeout` (a row an application transaction holds makes the
18
+ * batch give up and try again rather than queue behind it) and the
19
+ * statement timeout for a catalog change, which bounds how long one batch
20
+ * may hold its row locks.
21
+ *
22
+ * Per batch, the cycle core's `effect()` step runs
23
+ * (concepts/effect-receipts.mdx): read the receipt, skip the batch when it
24
+ * matches, otherwise update and write the receipt last, on success only.
25
+ * The batches are only known when the run reads the table, so the cycle runs
26
+ * here over the shared core's `ReceiptStore` seam (`./receipts.ts`), as the
27
+ * ClickHouse rebuild's does per partition. Unlike ClickHouse, the receipt
28
+ * commits in the batch's own transaction: a run killed anywhere leaves a
29
+ * batch with its receipt or neither, and a resumed backfill starts at the
30
+ * first batch with no receipt and updates no batch twice.
31
+ *
32
+ * Before each batch the backfill waits while a replica is further behind
33
+ * than the bound (`./replication.ts`).
34
+ *
35
+ * ## Verification
36
+ *
37
+ * One statement over the whole table, one snapshot: the rows, the rows whose
38
+ * new column differs from the expression over the old one, the new column's
39
+ * NULLs, and a checksum (the sum of a 64-bit hash of each value) of the new
40
+ * column and of the expression. Any difference, or a NULL where the column
41
+ * is declared NOT NULL, fails the run, whose onFailure then drops what the
42
+ * expand added. The dual-write trigger keeps writes going during the
43
+ * verification consistent; the statement reads the table under ACCESS SHARE
44
+ * only.
45
+ */
46
+ import { EffectReceipt, receiptExpectation } from "@intentius/chant/effect-receipt";
47
+ import { computePlanDigest } from "@intentius/chant/op";
48
+ import { col } from "./names.js";
49
+ import { MigrationRefusal, migrationPlanSubject } from "./observe.js";
50
+ import { postgresReceiptStore, receiptAddress } from "./receipts.js";
51
+ import { waitForReplicas } from "./replication.js";
52
+ import { batchPrefix, identityOf, inTransaction, observe } from "./steps.js";
53
+ import { carriedNotReady, requiresNotNull } from "./carry.js";
54
+ import { integerBatches, isIntegerKey, recordedBatches } from "./batches.js";
55
+ /** The effect a batch is, and its receipt's address suffix. */
56
+ export const batchEffect = (key, batch) => `migrate/${key}/${batch}`;
57
+ /** The receipt's expected value for one batch: bound to the table, the new column's attribute number, the batch's rows and the expression. */
58
+ export function batchExpectation(o, batch) {
59
+ const effect = batchEffect(o.names.key, batch.id);
60
+ return receiptExpectation(EffectReceipt(effect, {
61
+ effect,
62
+ flavor: "hash",
63
+ inputs: { migration: o.names.key, table: o.oid, column: o.newColumn.attnum, ...batch.inputs, expression: o.expression },
64
+ }));
65
+ }
66
+ /** The address suffix of the recorded batch boundaries of a key that is not one integer column. */
67
+ export const BATCHES_RECORD = "batches";
68
+ export async function backfill(run, deps = {}) {
69
+ const o = await observe(run);
70
+ const result = { state: o.state, batches: 0, filled: 0, skipped: 0, rows: 0, pausedMs: 0 };
71
+ if (o.state !== "migrate")
72
+ return result;
73
+ const n = o.names;
74
+ if (!o.newColumn)
75
+ throw new MigrationRefusal(`${n.key}: the new column is not there yet; the Expand phase adds it`);
76
+ if (!o.trigger)
77
+ throw new MigrationRefusal(`${n.key}: the dual write is not on; the Dual write phase starts it, and the backfill fills only rows written before it`);
78
+ if (!Number.isInteger(run.batchSize) || run.batchSize < 1)
79
+ throw new MigrationRefusal(`${n.key}: batchSize must be a positive integer, got ${run.batchSize}`);
80
+ const identity = identityOf(run.marker);
81
+ const receipts = deps.receipts ?? postgresReceiptStore(run.client, n.schema, identity, run.runId ? { runId: run.runId } : {});
82
+ await receipts.ensure();
83
+ const recorded = await receipts.readAll(batchPrefix(identity, n.key));
84
+ const batches = isIntegerKey(o.keyColumns)
85
+ ? await integerBatches(run.client, n.qualifiedTable, o.keyColumns[0], run.batchSize)
86
+ : (await recordedBatches(run.client, {
87
+ table: n.qualifiedTable,
88
+ oid: o.oid,
89
+ attnum: o.newColumn.attnum,
90
+ keys: o.keyColumns,
91
+ size: run.batchSize,
92
+ effect: batchEffect(n.key, BATCHES_RECORD),
93
+ receipts,
94
+ log: run.log,
95
+ ...(run.signal ? { signal: run.signal } : {}),
96
+ })).batches;
97
+ result.batches = batches.length;
98
+ const wait = deps.waitForReplicas ?? waitForReplicas;
99
+ const target = `${col(n.newColumn)}`;
100
+ for (const b of batches) {
101
+ run.signal?.throwIfAborted();
102
+ const effect = batchEffect(n.key, b.id);
103
+ const expectation = batchExpectation(o, b);
104
+ if (recorded.get(receiptAddress(identity, effect)) === expectation) {
105
+ result.skipped++;
106
+ continue;
107
+ }
108
+ if (run.replicationLag)
109
+ result.pausedMs += await wait(run.client, run.replicationLag, { log: run.log, ...(run.signal ? { signal: run.signal } : {}) });
110
+ const updated = await inTransaction(run, async (exec) => {
111
+ const [row] = await exec(`WITH u AS (UPDATE ${n.qualifiedTable} SET ${target} = (${o.expression}) WHERE ${b.where} AND ${target} IS DISTINCT FROM (${o.expression}) RETURNING 1) ` +
112
+ "SELECT count(*)::int AS n FROM u", b.params);
113
+ // The receipt, last, in the batch's own transaction.
114
+ await receipts.write({ name: effect, effect, flavor: "hash", inputs: {} }, expectation);
115
+ await deps.beforeCommit?.(b.id);
116
+ return Number(row?.n ?? 0);
117
+ });
118
+ result.rows += updated;
119
+ result.filled++;
120
+ await deps.afterBatch?.(b.id);
121
+ }
122
+ run.log(`-- backfill of ${n.key}: ${result.batches} batch(es), ${result.filled} filled (${result.rows} row(s)), ${result.skipped} already filled`);
123
+ return result;
124
+ }
125
+ /** A verification that found the columns different. */
126
+ export class MigrationVerificationError extends Error {
127
+ constructor(key, result, reason) {
128
+ super(`${key}: ${reason}. Nothing was switched; onFailure drops what the expand added, and the next run starts again.`);
129
+ this.key = key;
130
+ this.result = result;
131
+ this.name = "MigrationVerificationError";
132
+ }
133
+ }
134
+ export async function verifyMigration(run) {
135
+ const o = await observe(run);
136
+ const empty = { rows: 0, mismatched: 0, nulls: 0, checksum: "0", expected: "0" };
137
+ if (o.state !== "migrate")
138
+ return { state: o.state, ...empty, summary: `nothing to verify: ${o.state}` };
139
+ const n = o.names;
140
+ if (!o.newColumn || !o.trigger)
141
+ throw new MigrationRefusal(`${n.key}: the new column and its dual write are not there; the Expand and Dual write phases make them`);
142
+ const nw = col(n.newColumn);
143
+ const [row] = await inTransaction(run, (exec) => exec(`SELECT count(*)::text AS rows, count(*) FILTER (WHERE ${nw} IS DISTINCT FROM (${o.expression}))::text AS mismatched,
144
+ count(*) FILTER (WHERE ${nw} IS NULL)::text AS nulls,
145
+ COALESCE(sum(pg_catalog.hashtextextended(${nw}::text, 0)), 0)::text AS checksum,
146
+ COALESCE(sum(pg_catalog.hashtextextended((${o.expression})::text, 0)), 0)::text AS expected
147
+ FROM ${n.qualifiedTable}`), { scan: true });
148
+ const r = { rows: Number(row?.rows ?? 0), mismatched: Number(row?.mismatched ?? 0), nulls: Number(row?.nulls ?? 0), checksum: String(row?.checksum ?? "0"), expected: String(row?.expected ?? "0") };
149
+ if (r.mismatched > 0 || r.checksum !== r.expected) {
150
+ throw new MigrationVerificationError(n.key, { state: o.state, ...r }, `${r.mismatched} of ${r.rows} row(s) have ${n.newColumn} different from ${o.expression} (checksums ${r.checksum} and ${r.expected})`);
151
+ }
152
+ if (requiresNotNull(o.column, o.carried) && r.nulls > 0) {
153
+ throw new MigrationVerificationError(n.key, { state: o.state, ...r }, `${r.nulls} of ${r.rows} row(s) have ${n.newColumn} NULL, and ${n.column} is declared NOT NULL or is in the primary key`);
154
+ }
155
+ const notReady = carriedNotReady(o.carried, o.carriedStates);
156
+ if (notReady.length > 0) {
157
+ throw new MigrationRefusal(`${n.key}: ${notReady.map((c) => `${c.kind} ${c.working}`).join(", ")} on the new column is not there or not yet valid; the Carry over phase makes them`);
158
+ }
159
+ const carried = o.carried.length > 0 || o.views.length > 0
160
+ ? `; ${o.carried.map((c) => `${c.kind} ${c.name}${c.target !== c.name ? ` (as ${c.target})` : ""}`).join(", ") || "no index or constraint"} carried over${o.views.length > 0 ? `, view(s) ${o.views.map((v) => v.name).join(", ")} made again at the switch` : ""}`
161
+ : "";
162
+ const summary = `${n.key}: ${r.rows} row(s), ${n.newColumn} equal to ${o.expression} in every one (checksum ${r.checksum})${carried}`;
163
+ run.log(`-- ${summary}`);
164
+ return {
165
+ state: o.state,
166
+ planDigest: computePlanDigest("postgres-migration", { plan: migrationPlanSubject(o), table: o.oid, column: o.newColumn.attnum, verified: { mismatched: 0 } }),
167
+ ...r,
168
+ summary,
169
+ };
170
+ }