@intentius/chant-lexicon-sql 0.124.31 → 0.124.33

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,122 @@
1
+ /**
2
+ * How the backfill cuts a table into batches (#3281, #3322). A batch must be
3
+ * the same rows on every run, or a receipt written for it by one run would
4
+ * vouch for different rows in the next.
5
+ *
6
+ * - A primary key of one smallint, integer or bigint column: ranges of its
7
+ * value, `[b * size, (b + 1) * size)`. Arithmetic, so nothing needs to be
8
+ * remembered, and a batch is the same range however many rows were
9
+ * written since.
10
+ * - Any other primary key (uuid, text, a timestamp, several columns):
11
+ * ranges between recorded boundaries in the key's order. The first run
12
+ * walks the key's index once, taking every `size`-th key as a boundary
13
+ * (`WHERE key >= boundary ORDER BY key OFFSET size LIMIT 1`, so the walk
14
+ * reads each key once and sorts nothing), and records the boundaries in
15
+ * the receipt store, beside the batches' receipts, before any batch runs.
16
+ * Every later run reads them back instead of walking again, so batch `i`
17
+ * is `boundary[i] <= key < boundary[i + 1]` in every run. The first batch
18
+ * has no lower bound and the last no upper one, so a row written since,
19
+ * anywhere in the key's order, falls in exactly one batch (and the dual
20
+ * write has already filled it). Each batch is a range scan of the primary
21
+ * key's index, with a row comparison (`(a, b) >= ($1, $2)`) in the index's
22
+ * own column order.
23
+ *
24
+ * Hash buckets (`hash(key) % n`) were the other option: they need nothing
25
+ * recorded but the bucket count, but no index can serve them, so every batch
26
+ * would read the whole table. Ranges recorded once read each row once.
27
+ *
28
+ * The record is bound to the table's oid and the new column's attribute
29
+ * number, like the batches' receipts: a migration started again after
30
+ * onFailure (which drops the new column and the receipts) walks the key
31
+ * again. A record left from another batch size is kept, since its batches'
32
+ * receipts are what the run resumes from.
33
+ */
34
+ import { col } from "./names.js";
35
+ export const INTEGER_KEY_TYPES = new Set(["smallint", "integer", "bigint"]);
36
+ /** Whether the key is batched by arithmetic ranges of its one integer column. */
37
+ export const isIntegerKey = (keys) => keys.length === 1 && INTEGER_KEY_TYPES.has(keys[0].type);
38
+ /** `id`, or `(tenant_id, id)`. */
39
+ export const keyText = (keys) => (keys.length === 1 ? keys[0].name : `(${keys.map((k) => k.name).join(", ")})`);
40
+ const tuple = (keys) => (keys.length === 1 ? col(keys[0].name) : `(${keys.map((k) => col(k.name)).join(", ")})`);
41
+ const placeholders = (keys, from) => {
42
+ const p = keys.map((k, i) => `$${from + i}::${k.type}`);
43
+ return keys.length === 1 ? p[0] : `(${p.join(", ")})`;
44
+ };
45
+ /** The batches of an integer key: one per range that holds rows. */
46
+ export async function integerBatches(client, table, key, size) {
47
+ const k = col(key.name);
48
+ const rows = await client.query(`SELECT pg_catalog.floor(${k}::numeric / ${size})::bigint::text AS b FROM ${table} GROUP BY 1 ORDER BY 1`);
49
+ return rows.map(({ b }) => {
50
+ const lo = (BigInt(b) * BigInt(size)).toString();
51
+ const hi = ((BigInt(b) + 1n) * BigInt(size)).toString();
52
+ return { id: b, where: `${k} >= $1 AND ${k} < $2`, params: [lo, hi], inputs: { batch: b, size, key: key.name } };
53
+ });
54
+ }
55
+ /** The batches between recorded boundaries: `[undefined, b1)`, `[b1, b2)`, ..., `[bn, undefined)`, where `b0` is only where the walk started. */
56
+ export function boundaryBatches(keys, bounds) {
57
+ const k = tuple(keys);
58
+ return bounds.map((lo, i) => {
59
+ const hi = bounds[i + 1];
60
+ const where = [];
61
+ const params = [];
62
+ if (i > 0) {
63
+ where.push(`${k} >= ${placeholders(keys, 1)}`);
64
+ params.push(...lo);
65
+ }
66
+ if (hi) {
67
+ where.push(`${k} < ${placeholders(keys, params.length + 1)}`);
68
+ params.push(...hi);
69
+ }
70
+ return {
71
+ id: `k${i}`,
72
+ where: where.join(" AND ") || "true",
73
+ params,
74
+ inputs: { batch: `k${i}`, key: keys.map((c) => c.name), lo: i > 0 ? lo : null, hi: hi ?? null },
75
+ };
76
+ });
77
+ }
78
+ /** Walk the key's order and take every `size`-th key: the boundaries the batches lie between. */
79
+ export async function walkBoundaries(client, table, keys, size, signal) {
80
+ const select = keys.map((k, i) => `${col(k.name)}::text AS k${i}`).join(", ");
81
+ const order = keys.map((k) => col(k.name)).join(", ");
82
+ const values = (row) => (row ? keys.map((_, i) => String(row[`k${i}`])) : undefined);
83
+ const bounds = [];
84
+ let at = values((await client.query(`SELECT ${select} FROM ${table} ORDER BY ${order} LIMIT 1`))[0]);
85
+ while (at) {
86
+ signal?.throwIfAborted();
87
+ bounds.push(at);
88
+ at = values((await client.query(`SELECT ${select} FROM ${table} WHERE ${tuple(keys)} >= ${placeholders(keys, 1)} ORDER BY ${order} OFFSET ${size} LIMIT 1`, at))[0]);
89
+ }
90
+ return bounds;
91
+ }
92
+ /**
93
+ * The boundary batches of a key that is not one integer column: read from
94
+ * the record in the receipt store, or walked and recorded when there is
95
+ * none for this table and new column.
96
+ */
97
+ export async function recordedBatches(client, input) {
98
+ const ref = { name: input.effect, effect: input.effect, flavor: "hash", inputs: {} };
99
+ const keys = input.keys.map((k) => k.name);
100
+ const raw = await input.receipts.read(ref);
101
+ let record;
102
+ try {
103
+ const parsed = raw === undefined ? undefined : JSON.parse(raw);
104
+ if (parsed?.v === 1 && parsed.table === input.oid && parsed.column === input.attnum && JSON.stringify(parsed.keys) === JSON.stringify(keys))
105
+ record = parsed;
106
+ }
107
+ catch {
108
+ record = undefined;
109
+ }
110
+ let walked = false;
111
+ if (!record) {
112
+ const bounds = await walkBoundaries(client, input.table, input.keys, input.size, input.signal);
113
+ record = { v: 1, table: input.oid, column: input.attnum, keys, size: input.size, bounds };
114
+ await input.receipts.write(ref, JSON.stringify(record));
115
+ walked = true;
116
+ input.log(`-- batches of ${keyText(input.keys)}: ${bounds.length} boundary(ies) every ${input.size} key(s), recorded`);
117
+ }
118
+ else if (record.size !== input.size) {
119
+ input.log(`-- batches of ${keyText(input.keys)}: kept as recorded every ${record.size} key(s); batchSize ${input.size} applies to a migration started again`);
120
+ }
121
+ return { batches: boundaryBatches(input.keys, record.bounds), record, walked };
122
+ }
@@ -0,0 +1,518 @@
1
+ /**
2
+ * What uses the migrated column, carried over to the new one (#3322).
3
+ *
4
+ * An index, a constraint or a view on the column binds the column by its
5
+ * attribute number, not its name, so a swap of names at the switch would
6
+ * leave it on the old column, and the contract could not drop that column
7
+ * while something uses it. Each is made again on the new column before the
8
+ * switch, under a working name, and the switch swaps the names:
9
+ *
10
+ * | On the old column | Before the switch | In the switch's transaction |
11
+ * |---|---|---|
12
+ * | an index | `CREATE INDEX CONCURRENTLY` on the new column | the old one dropped (a type change) or kept as `<name>__chant_old` (a rename), the new one renamed to the name |
13
+ * | a primary key or unique constraint | its unique index built `CONCURRENTLY` | the old constraint dropped (or a rename's unique one kept as `__chant_old`), `ADD CONSTRAINT ... USING INDEX` |
14
+ * | a check or foreign key, this table's or another's that references the column | added `NOT VALID`, then `VALIDATE` | the old one dropped, the new one renamed to the name |
15
+ * | a view (and the views that read it) | nothing | dropped, then created again from its declaration once the columns are swapped, with its grants and owner |
16
+ *
17
+ * `CREATE INDEX CONCURRENTLY` reads the table without blocking writes, and
18
+ * `VALIDATE CONSTRAINT` holds SHARE UPDATE EXCLUSIVE, so both run while the
19
+ * application keeps writing; the dual-write trigger keeps the new column
20
+ * equal to the old one, so the index and the constraints hold the same rows
21
+ * as the old ones. A foreign key from another table is added against the
22
+ * new column's working unique index, which the switch then attaches to the
23
+ * key constraint. Everything in the switch changes only the catalog.
24
+ *
25
+ * Working objects carry chant's trailer with `migration=<key>` and
26
+ * `role=carry`, so the catalog read leaves them out of plans until the
27
+ * switch, and onFailure drops them (`./steps.ts`). The switch gives each its
28
+ * old comment back.
29
+ *
30
+ * Names: a type change keeps every name. A rename keeps a name the
31
+ * declaration does not give otherwise, except one Postgres made from the
32
+ * old column (`users_email_key` becomes `users_login_key`); a key, foreign
33
+ * key or index the build declares on the new column takes the declared name.
34
+ *
35
+ * Refused, each named with what to do: exclusion constraints and constraint
36
+ * triggers (no `USING INDEX` form), materialized views (made again only by
37
+ * a full scan in the switch), rules, triggers, policies and statistics on the
38
+ * column, a generated column computed from it, an INVALID index on it, a view
39
+ * the build does not declare, a view something other than a view depends on.
40
+ */
41
+ import { quoteIdent } from "../keywords.js";
42
+ import { tokenizeText } from "../tokens.js";
43
+ import { identValue } from "../parser.js";
44
+ import { createSteps, pgString } from "../apply/statements.js";
45
+ import { POSTGRES_ENTITY_TYPES } from "../entity-types.js";
46
+ import { canonicalName } from "../plan/normalize.js";
47
+ import { pgName } from "./names.js";
48
+ /** The working name of a carried object, and the name a rename keeps the old one under. */
49
+ export const carriedWorkingName = (name) => pgName(`${name}__chant_new`);
50
+ export const carriedOldName = (name) => pgName(`${name}__chant_old`);
51
+ /**
52
+ * `sql` with every reference to the column `from` replaced by `to`: an
53
+ * identifier token naming it, not a qualifier (`x.`), a qualified name's
54
+ * last part, a function's name, a cast's type or an access method. Within
55
+ * `[start, end)` token indexes when given.
56
+ */
57
+ export function replaceColumn(sql, from, to, range) {
58
+ const tokens = tokenizeText(sql, 0);
59
+ const ranges = range ? range(tokens) : [[0, tokens.length]];
60
+ const significant = (i, step) => {
61
+ for (let j = i + step; j >= 0 && j < tokens.length; j += step)
62
+ if (tokens[j].kind !== "ws" && tokens[j].kind !== "comment")
63
+ return tokens[j];
64
+ return undefined;
65
+ };
66
+ const inRange = (i) => ranges.some(([a, b]) => i >= a && i < b);
67
+ return tokens
68
+ .map((t, i) => {
69
+ if ((t.kind !== "ident" && t.kind !== "qident") || !inRange(i) || identValue(t) !== from)
70
+ return t.text;
71
+ const prev = significant(i, -1);
72
+ const next = significant(i, 1);
73
+ if (prev?.text === "." || next?.text === "." || next?.text === "(")
74
+ return t.text;
75
+ if (prev?.kind === "op" && prev.text === "::")
76
+ return t.text;
77
+ if (prev?.kind === "ident" && /^using$/i.test(prev.text))
78
+ return t.text;
79
+ return quoteIdent(to);
80
+ })
81
+ .join("");
82
+ }
83
+ /**
84
+ * A foreign key's token ranges: the referencing side (before `REFERENCES`,
85
+ * and an `ON DELETE SET NULL (...)` list after it) and the referenced
86
+ * column list.
87
+ */
88
+ function foreignKeyRanges(tokens) {
89
+ const at = tokens.findIndex((t) => t.kind === "ident" && /^references$/i.test(t.text));
90
+ if (at < 0)
91
+ return { local: [[0, tokens.length]], referenced: [] };
92
+ let open = at + 1;
93
+ while (open < tokens.length && !(tokens[open].kind === "punct" && tokens[open].text === "("))
94
+ open++;
95
+ let close = open;
96
+ for (let depth = 0; close < tokens.length; close++) {
97
+ if (tokens[close].text === "(")
98
+ depth++;
99
+ else if (tokens[close].text === ")" && --depth === 0)
100
+ break;
101
+ }
102
+ return { local: [[0, at], [close + 1, tokens.length]], referenced: open < tokens.length ? [[open, close + 1]] : [] };
103
+ }
104
+ /** A foreign key's body with the column replaced on the side(s) that are the migrated table. */
105
+ export function replaceInForeignKey(def, from, to, sides) {
106
+ return replaceColumn(def, from, to, (tokens) => {
107
+ const r = foreignKeyRanges(tokens);
108
+ return [...(sides.local ? r.local : []), ...(sides.referenced ? r.referenced : [])];
109
+ });
110
+ }
111
+ /** A constraint's body without a trailing `NOT VALID`, which the carry adds itself. */
112
+ const withoutNotValid = (def) => def.replace(/\s+NOT VALID\s*$/i, "");
113
+ /**
114
+ * `CREATE [UNIQUE] INDEX <name> ON ...` as `pg_get_indexdef` prints it, as
115
+ * the `CONCURRENTLY` build of `working` on the new column.
116
+ */
117
+ export function carriedIndexStatement(def, working, from, to) {
118
+ const m = /^CREATE (UNIQUE )?INDEX (?:"(?:[^"]|"")+"|\S+) ON (.*)$/s.exec(def);
119
+ if (!m)
120
+ throw new Error(`unexpected index definition: ${def}`);
121
+ return `CREATE ${m[1] ?? ""}INDEX CONCURRENTLY ${quoteIdent(working)} ON ${replaceColumn(m[2], from, to)}`;
122
+ }
123
+ /**
124
+ * The name Postgres gives an object it names after its columns
125
+ * (`users_email_key`, `orders_user_id_fkey`, `users_email_idx`), when `name`
126
+ * is that name for the old columns: the same name for the new ones. Only a
127
+ * name that fits untruncated; any other name is kept.
128
+ */
129
+ export function renamedDefault(name, table, oldCols, newCols, suffix) {
130
+ const made = (cols) => `${table}_${cols.join("_")}_${suffix}`;
131
+ if (name !== made(oldCols))
132
+ return undefined;
133
+ const next = made(newCols);
134
+ return Buffer.byteLength(next, "utf8") <= 63 && next !== name ? next : undefined;
135
+ }
136
+ const sameColumns = (a, b) => a.length === b.length && a.every((c, i) => c === b[i]);
137
+ /** What uses the column the values come from, sorted into what the Op carries over and what it refuses. */
138
+ export async function discoverDependents(input) {
139
+ const { client, names: n, oid, attnum } = input;
140
+ const from = n.source;
141
+ const to = n.newColumn;
142
+ const deps = await client.query(`SELECT d.deptype::text AS deptype, CASE d.classid WHEN 'pg_catalog.pg_constraint'::pg_catalog.regclass THEN 'constraint' WHEN 'pg_catalog.pg_class'::pg_catalog.regclass THEN 'class'
143
+ WHEN 'pg_catalog.pg_rewrite'::pg_catalog.regclass THEN 'rewrite' WHEN 'pg_catalog.pg_attrdef'::pg_catalog.regclass THEN 'attrdef' ELSE 'other' END AS kind,
144
+ d.objid::text AS objid, d.objsubid, pg_catalog.pg_describe_object(d.classid, d.objid, d.objsubid) AS what
145
+ FROM pg_catalog.pg_depend d
146
+ WHERE d.refclassid = 'pg_catalog.pg_class'::pg_catalog.regclass AND d.refobjid = $1::oid AND d.refobjsubid = $2 AND d.deptype IN ('n', 'a', 'i')
147
+ UNION
148
+ SELECT 'n', 'constraint', c.oid::text, 0, pg_catalog.pg_describe_object('pg_catalog.pg_constraint'::pg_catalog.regclass, c.oid, 0)
149
+ FROM pg_catalog.pg_constraint c WHERE c.confrelid = $1::oid AND $2 = ANY(c.confkey)`, [oid, attnum]);
150
+ const refused = [];
151
+ const carried = [];
152
+ let sequence;
153
+ const byKind = (k) => [...new Set(deps.filter((d) => d.kind === k).map((d) => d.objid))];
154
+ const table = n.table;
155
+ const rename = n.change === "rename";
156
+ const tprops = input.declared.props;
157
+ // ── constraints ─────────────────────────────────────────────────────
158
+ const constraintIds = byKind("constraint");
159
+ const keyIndexes = new Set();
160
+ if (constraintIds.length > 0) {
161
+ const rows = await client.query(`SELECT c.oid::text AS oid, c.conname AS name, c.contype::text AS type, c.conrelid::text AS rel, c.confrelid::text AS frel, c.conindid::text AS idx,
162
+ pg_catalog.pg_get_constraintdef(c.oid) AS def, c.convalidated AS validated, c.condeferrable AS deferrable, c.condeferred AS deferred,
163
+ pg_catalog.obj_description(c.oid, 'pg_constraint') AS comment, n.nspname AS schema, t.relname AS table_name,
164
+ CASE WHEN c.conindid <> 0 THEN pg_catalog.obj_description(c.conindid, 'pg_class') END AS index_comment,
165
+ CASE WHEN c.conindid <> 0 AND c.contype IN ('p', 'u') THEN pg_catalog.pg_get_indexdef(c.conindid) END AS indexdef,
166
+ COALESCE(i.indisreplident, false) AS replident, COALESCE(i.indisclustered, false) AS clustered,
167
+ (SELECT a.attname FROM pg_catalog.pg_attribute a WHERE a.attrelid = c.conrelid AND a.attnum = c.conkey[1]) AS first_col,
168
+ ARRAY(SELECT a.attname FROM pg_catalog.unnest(c.conkey) WITH ORDINALITY k(n, o) JOIN pg_catalog.pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.n ORDER BY k.o)::text[] AS cols,
169
+ t.relkind = 'p' OR EXISTS (SELECT 1 FROM pg_catalog.pg_class f WHERE f.oid = c.confrelid AND f.relkind = 'p') AS frel_partitioned
170
+ FROM pg_catalog.pg_constraint c JOIN pg_catalog.pg_class t ON t.oid = c.conrelid JOIN pg_catalog.pg_namespace n ON n.oid = t.relnamespace
171
+ LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.conindid
172
+ WHERE c.oid = ANY($1::oid[]) ORDER BY c.conname`, [constraintIds]);
173
+ for (const r of rows) {
174
+ const name = String(r.name);
175
+ const own = String(r.rel) === oid;
176
+ const where = `${String(r.schema)}.${String(r.table_name)}`;
177
+ const qtable = `${quoteIdent(String(r.schema))}.${quoteIdent(String(r.table_name))}`;
178
+ const type = String(r.type);
179
+ if (type === "n" && own)
180
+ continue; // the column's own NOT NULL (18)
181
+ const comment = r.comment === null ? undefined : String(r.comment);
182
+ const base = { name, table: qtable, tableName: where, tableOid: String(r.rel), schema: String(r.schema), working: carriedWorkingName(name), validated: r.validated === true, ...(comment !== undefined ? { comment } : {}) };
183
+ const cols = r.cols ?? [];
184
+ const newCols = cols.map((c) => (own && c === from ? n.column : c));
185
+ if (input.partitioned && (type === "p" || type === "u" || type === "f")) {
186
+ keyIndexes.add(String(r.idx));
187
+ refused.push(`${type === "f" ? "foreign key" : type === "p" ? "primary key" : "unique constraint"} ${name} on ${where}: on a partitioned table ${type === "f" ? "a foreign key cannot be added NOT VALID and validated while writes go on" : "a key cannot be attached from an index built CONCURRENTLY (ADD CONSTRAINT ... USING INDEX is not supported on partitioned tables)"}, so carrying it over would hold ACCESS EXCLUSIVE for a full build; drop it, migrate, then declare it again (chant #3333)`);
188
+ continue;
189
+ }
190
+ if (type === "f" && !own && r.frel_partitioned === true) {
191
+ refused.push(`foreign key ${name} on ${where} references a partitioned table, where it cannot be added NOT VALID and validated while writes go on; drop it, migrate, then declare it again`);
192
+ continue;
193
+ }
194
+ if (type === "p" || type === "u") {
195
+ keyIndexes.add(String(r.idx));
196
+ const declaredKey = type === "p" ? (tprops.primaryKey?.name ? tprops.primaryKey : undefined) : tprops.uniques?.find((u) => u.name && sameColumns(u.columns, newCols));
197
+ const target = !rename ? name : (declaredKey?.name ?? (type === "u" ? renamedDefault(name, table, cols, newCols, "key") : undefined) ?? name);
198
+ const deferral = r.deferrable === true ? ` DEFERRABLE${r.deferred === true ? " INITIALLY DEFERRED" : ""}` : "";
199
+ carried.push({
200
+ ...base,
201
+ kind: "key",
202
+ target,
203
+ primary: type === "p",
204
+ ...(deferral ? { deferral } : {}),
205
+ definition: carriedIndexStatement(String(r.indexdef), base.working, from, to),
206
+ ...(r.index_comment !== null && r.index_comment !== undefined ? { indexComment: String(r.index_comment) } : {}),
207
+ ...(r.replident === true ? { replicaIdentity: true } : {}),
208
+ ...(r.clustered === true ? { clustered: true } : {}),
209
+ });
210
+ }
211
+ else if (type === "c") {
212
+ const first = String(r.first_col ?? "");
213
+ const target = !rename ? name : (renamedDefault(name, table, [first], [first === from ? n.column : first], "check") ?? name);
214
+ carried.push({ ...base, kind: "check", target, definition: replaceColumn(withoutNotValid(String(r.def)), from, to) });
215
+ }
216
+ else if (type === "f") {
217
+ const referenced = String(r.frel) === oid;
218
+ const def = replaceInForeignKey(withoutNotValid(String(r.def)), from, to, { local: own, referenced });
219
+ let target = name;
220
+ if (rename && own) {
221
+ const declaredFk = tprops.foreignKeys?.find((f) => f.name && sameColumns(f.columns, newCols));
222
+ target = declaredFk?.name ?? renamedDefault(name, table, cols, newCols, "fkey") ?? name;
223
+ }
224
+ carried.push({ ...base, kind: "foreign-key", target, definition: def });
225
+ }
226
+ else if (type === "x") {
227
+ refused.push(`exclusion constraint ${name} on ${where}: an exclusion constraint cannot be attached from an index built CONCURRENTLY, so adding it again would hold ACCESS EXCLUSIVE for a full build; drop it, migrate, then declare it again`);
228
+ }
229
+ else if (type === "t") {
230
+ refused.push(`constraint trigger ${name} on ${where}: its function reads the column by name, which the Op cannot rewrite; drop it, migrate, then create it again`);
231
+ }
232
+ else {
233
+ refused.push(`constraint ${name} on ${where} (type ${type})`);
234
+ }
235
+ }
236
+ }
237
+ // ── indexes, sequences, columns ─────────────────────────────────────
238
+ const classIds = byKind("class");
239
+ if (classIds.length > 0) {
240
+ const rows = await client.query(`SELECT c.oid::text AS oid, c.relname AS name, c.relkind::text AS kind, n.nspname AS schema, pg_catalog.obj_description(c.oid, 'pg_class') AS comment,
241
+ CASE WHEN c.relkind IN ('i', 'I') THEN pg_catalog.pg_get_indexdef(c.oid) END AS def,
242
+ i.indisvalid AS valid, i.indisunique AS is_unique, COALESCE(i.indisreplident, false) AS replident, COALESCE(i.indisclustered, false) AS clustered,
243
+ ARRAY(SELECT a.attname FROM pg_catalog.unnest(i.indkey::int2[]) WITH ORDINALITY k(n, o) JOIN pg_catalog.pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.n ORDER BY k.o)::text[] AS cols,
244
+ i.indexprs IS NOT NULL OR i.indpred IS NOT NULL AS has_exprs
245
+ FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid
246
+ WHERE c.oid = ANY($1::oid[]) ORDER BY c.relname`, [classIds]);
247
+ const declaredIndexes = input.objects.filter((o) => o.type === POSTGRES_ENTITY_TYPES.index);
248
+ for (const r of rows) {
249
+ const name = String(r.name);
250
+ const kind = String(r.kind);
251
+ if (kind === "i" || kind === "I") {
252
+ if (keyIndexes.has(String(r.oid)))
253
+ continue;
254
+ if (kind === "I") {
255
+ refused.push(`index ${r.schema}.${name} is a partitioned index, which cannot be built CONCURRENTLY; drop it, migrate, then declare it again (chant #3333)`);
256
+ continue;
257
+ }
258
+ if (r.valid !== true) {
259
+ refused.push(`index ${r.schema}.${name} is INVALID (a failed CONCURRENTLY build); drop it, or REINDEX it, first`);
260
+ continue;
261
+ }
262
+ const working = carriedWorkingName(name);
263
+ let target = name;
264
+ if (rename) {
265
+ const oldCols = r.cols ?? [];
266
+ const cols = oldCols.map((c) => (c === from ? n.column : c));
267
+ const declaredIndex = r.has_exprs === true
268
+ ? undefined
269
+ : declaredIndexes.find((d) => {
270
+ const p = d.props;
271
+ const els = p.elements ?? [];
272
+ return (canonicalName(String(p.tableName ?? ""), input.defaultSchema) === `${n.schema}.${n.table}` &&
273
+ (p.unique === true) === (r.is_unique === true) &&
274
+ !p.where &&
275
+ els.every((e) => e.column !== undefined) &&
276
+ sameColumns(els.map((e) => e.column), cols));
277
+ });
278
+ target = declaredIndex?.canonical.name ?? (r.has_exprs === true ? undefined : renamedDefault(name, table, oldCols, cols, "idx")) ?? name;
279
+ }
280
+ carried.push({
281
+ kind: "index",
282
+ name,
283
+ table: n.qualifiedTable,
284
+ tableName: `${n.schema}.${n.table}`,
285
+ tableOid: oid,
286
+ schema: String(r.schema),
287
+ working,
288
+ target,
289
+ definition: carriedIndexStatement(String(r.def), working, from, to),
290
+ validated: true,
291
+ ...(r.comment !== null ? { comment: String(r.comment) } : {}),
292
+ ...(r.replident === true ? { replicaIdentity: true } : {}),
293
+ ...(r.clustered === true ? { clustered: true } : {}),
294
+ });
295
+ }
296
+ else if (kind === "S") {
297
+ const dep = deps.find((d) => d.kind === "class" && d.objid === String(r.oid));
298
+ const seq = { kind: dep?.deptype === "i" ? "identity" : "serial", schema: String(r.schema), name, ident: `${quoteIdent(String(r.schema))}.${quoteIdent(name)}` };
299
+ if (rename) {
300
+ refused.push(`sequence ${r.schema}.${name} gives the column its values (${seq.kind === "identity" ? "an identity column" : "a serial column's default"}); during a rename a writer that names the new column would be given the old one's next value by the trigger, which it cannot tell from a value the writer chose; a rename that keeps its sequence needs versioned views (chant #3331)`);
301
+ }
302
+ else
303
+ sequence = seq;
304
+ }
305
+ else {
306
+ const what = deps.find((d) => d.kind === "class" && d.objid === String(r.oid))?.what ?? `${r.schema}.${name}`;
307
+ refused.push(what);
308
+ }
309
+ }
310
+ }
311
+ // ── generated columns computed from it ──────────────────────────────
312
+ const attrdefIds = byKind("attrdef");
313
+ if (attrdefIds.length > 0) {
314
+ const rows = await client.query("SELECT a.attname AS name, d.adnum FROM pg_catalog.pg_attrdef d JOIN pg_catalog.pg_attribute a ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE d.oid = ANY($1::oid[])", [attrdefIds]);
315
+ for (const r of rows) {
316
+ if (Number(r.adnum) === attnum)
317
+ continue; // its own default
318
+ refused.push(`generated column ${n.schema}.${n.table}.${r.name} is computed from it; computing it from the new column would rewrite the table, so drop the generated column, migrate, then declare it again`);
319
+ }
320
+ }
321
+ // ── views, rules ────────────────────────────────────────────────────
322
+ const views = [];
323
+ const rewriteIds = byKind("rewrite");
324
+ if (rewriteIds.length > 0) {
325
+ const rows = await client.query(`WITH RECURSIVE v(oid, depth) AS (
326
+ SELECT r.ev_class, 0 FROM pg_catalog.pg_rewrite r WHERE r.oid = ANY($1::oid[]) AND r.ev_class <> $2::oid
327
+ UNION
328
+ SELECT r.ev_class, v.depth + 1 FROM v
329
+ JOIN pg_catalog.pg_depend d ON d.refclassid = 'pg_catalog.pg_class'::pg_catalog.regclass AND d.refobjid = v.oid AND d.classid = 'pg_catalog.pg_rewrite'::pg_catalog.regclass
330
+ JOIN pg_catalog.pg_rewrite r ON r.oid = d.objid AND r.ev_class <> v.oid
331
+ )
332
+ SELECT c.oid::text AS oid, n.nspname AS schema, c.relname AS name, c.relkind::text AS kind, max(v.depth) AS depth,
333
+ pg_catalog.pg_get_userbyid(c.relowner) AS owner,
334
+ ARRAY(SELECT CASE WHEN x.grantee = 0 THEN 'PUBLIC' ELSE pg_catalog.quote_ident(pg_catalog.pg_get_userbyid(x.grantee)) END || E'\\t' || x.privilege_type || E'\\t' || x.is_grantable::text
335
+ FROM pg_catalog.aclexplode(c.relacl) x WHERE x.grantee <> c.relowner)::text[] AS grants,
336
+ ARRAY(SELECT pg_catalog.pg_describe_object(d.classid, d.objid, d.objsubid) FROM pg_catalog.pg_depend d
337
+ WHERE d.refclassid = 'pg_catalog.pg_class'::pg_catalog.regclass AND d.refobjid = c.oid AND d.deptype = 'n'
338
+ AND d.classid NOT IN ('pg_catalog.pg_rewrite'::pg_catalog.regclass, 'pg_catalog.pg_type'::pg_catalog.regclass))::text[] AS others
339
+ FROM v JOIN pg_catalog.pg_class c ON c.oid = v.oid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
340
+ GROUP BY c.oid, n.nspname, c.relname, c.relkind, c.relowner, c.relacl ORDER BY 5, 2, 3`, [rewriteIds, oid]);
341
+ const ownRules = await client.query("SELECT r.rulename AS name FROM pg_catalog.pg_rewrite r WHERE r.oid = ANY($1::oid[]) AND r.ev_class = $2::oid", [rewriteIds, oid]);
342
+ for (const r of ownRules)
343
+ refused.push(`rule ${r.name} on ${n.schema}.${n.table} reads the column; drop it, migrate, then create it again`);
344
+ for (const r of rows) {
345
+ const name = `${String(r.schema)}.${String(r.name)}`;
346
+ if (r.kind === "m") {
347
+ refused.push(`materialized view ${name} reads the column; making it again would scan in the switch's transaction, so drop it, migrate, then declare it again`);
348
+ continue;
349
+ }
350
+ if (r.kind !== "v") {
351
+ refused.push(`${name} (relkind ${String(r.kind)}) reads the column`);
352
+ continue;
353
+ }
354
+ const others = r.others ?? [];
355
+ if (others.length > 0)
356
+ refused.push(`view ${name} is used by ${others.join(", ")}, which dropping and creating the view again would break`);
357
+ const declared = input.objects.find((o) => (o.type === POSTGRES_ENTITY_TYPES.view || o.type === POSTGRES_ENTITY_TYPES.materializedView) && `${o.canonical.schema ?? input.defaultSchema}.${o.canonical.name}` === name);
358
+ if (!declared) {
359
+ refused.push(`view ${name} reads ${rename ? `${from}, which the rename takes away` : "the column"}, and the build does not declare it; the switch makes each such view again from its declaration, so declare it (reading ${n.column}) in the build`);
360
+ continue;
361
+ }
362
+ if (declared.type !== POSTGRES_ENTITY_TYPES.view) {
363
+ refused.push(`view ${name} is declared as a ${declared.type}`);
364
+ continue;
365
+ }
366
+ views.push({
367
+ name,
368
+ ident: `${quoteIdent(String(r.schema))}.${quoteIdent(String(r.name))}`,
369
+ materialized: false,
370
+ depth: Number(r.depth),
371
+ declared,
372
+ owner: String(r.owner),
373
+ grants: (r.grants ?? []).map((g) => {
374
+ const [grantee, privilege, grantable] = g.split("\t");
375
+ return { grantee, privilege, grantable: grantable === "true" };
376
+ }),
377
+ });
378
+ }
379
+ }
380
+ for (const d of deps.filter((x) => x.kind === "other"))
381
+ refused.push(`${d.what} uses the column; drop it, migrate, then create it again`);
382
+ // A new name already taken by something else stops the switch; say so now.
383
+ const renamed = carried.filter((c) => c.target !== c.name);
384
+ if (renamed.length > 0) {
385
+ const taken = await client.query(`SELECT x.name FROM ROWS FROM (pg_catalog.unnest($1::text[]), pg_catalog.unnest($2::text[]), pg_catalog.unnest($3::oid[])) AS x(name, schema, rel)
386
+ WHERE EXISTS (SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace s ON s.oid = c.relnamespace WHERE s.nspname = x.schema AND c.relname = x.name)
387
+ OR EXISTS (SELECT 1 FROM pg_catalog.pg_constraint k WHERE k.conrelid = x.rel AND k.conname = x.name)`, [renamed.map((c) => c.target), renamed.map((c) => c.schema), renamed.map((c) => c.tableOid)]);
388
+ for (const t of taken)
389
+ refused.push(`${t.name}, the name ${renamed.find((c) => c.target === t.name).name} takes after the rename, is already used; rename that object first`);
390
+ }
391
+ return { carried, views, refused, ...(sequence ? { sequence } : {}) };
392
+ }
393
+ // ── statements ──────────────────────────────────────────────────────
394
+ /** `ALTER TABLE ... ADD CONSTRAINT <working> <body> NOT VALID`: a check or foreign key on the new column. */
395
+ export const addCarriedConstraint = (c) => `ALTER TABLE ${c.table} ADD CONSTRAINT ${quoteIdent(c.working)} ${c.definition} NOT VALID`;
396
+ const commentOn = (what, text) => `COMMENT ON ${what} IS ${text === undefined ? "NULL" : pgString(text)}`;
397
+ const indexIdent = (c, name) => `${quoteIdent(c.schema)}.${quoteIdent(name)}`;
398
+ /**
399
+ * The switch's statements for the carried objects, around the swap of the
400
+ * columns: `before` drops the views and swaps every index and constraint;
401
+ * `after` creates the views again from their declarations once the column
402
+ * they read has its name.
403
+ *
404
+ * Foreign keys that reference the column go first: the old key constraint
405
+ * cannot be dropped while one depends on its index.
406
+ */
407
+ export function carriedSwitchStatements(carried, views, change, oldComment, marker) {
408
+ const before = [];
409
+ const after = [];
410
+ for (const v of [...views].sort((a, b) => b.depth - a.depth))
411
+ before.push(`DROP VIEW ${v.ident}`);
412
+ const order = ["foreign-key", "check", "key", "index"];
413
+ const sorted = [...carried].sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind));
414
+ for (const c of sorted) {
415
+ const keepOld = change === "rename" && (c.kind === "index" || (c.kind === "key" && !c.primary));
416
+ if (c.kind === "check" || c.kind === "foreign-key") {
417
+ before.push(`ALTER TABLE ${c.table} DROP CONSTRAINT ${quoteIdent(c.name)}`);
418
+ before.push(`ALTER TABLE ${c.table} RENAME CONSTRAINT ${quoteIdent(c.working)} TO ${quoteIdent(c.target)}`);
419
+ before.push(commentOn(`CONSTRAINT ${quoteIdent(c.target)} ON ${c.table}`, c.comment));
420
+ }
421
+ else if (c.kind === "key") {
422
+ if (keepOld) {
423
+ before.push(`ALTER TABLE ${c.table} RENAME CONSTRAINT ${quoteIdent(c.name)} TO ${quoteIdent(carriedOldName(c.name))}`);
424
+ before.push(commentOn(`CONSTRAINT ${quoteIdent(carriedOldName(c.name))} ON ${c.table}`, oldComment(c)));
425
+ }
426
+ else {
427
+ before.push(`ALTER TABLE ${c.table} DROP CONSTRAINT ${quoteIdent(c.name)}`);
428
+ }
429
+ before.push(`ALTER TABLE ${c.table} ADD CONSTRAINT ${quoteIdent(c.target)} ${c.primary ? "PRIMARY KEY" : "UNIQUE"} USING INDEX ${quoteIdent(c.working)}${c.deferral ?? ""}`);
430
+ before.push(commentOn(`CONSTRAINT ${quoteIdent(c.target)} ON ${c.table}`, c.comment));
431
+ before.push(commentOn(`INDEX ${indexIdent(c, c.target)}`, c.indexComment));
432
+ }
433
+ else {
434
+ if (keepOld) {
435
+ before.push(`ALTER INDEX ${indexIdent(c, c.name)} RENAME TO ${quoteIdent(carriedOldName(c.name))}`);
436
+ before.push(commentOn(`INDEX ${indexIdent(c, carriedOldName(c.name))}`, oldComment(c)));
437
+ }
438
+ else {
439
+ before.push(`DROP INDEX ${indexIdent(c, c.name)}`);
440
+ }
441
+ before.push(`ALTER INDEX ${indexIdent(c, c.working)} RENAME TO ${quoteIdent(c.target)}`);
442
+ before.push(commentOn(`INDEX ${indexIdent(c, c.target)}`, c.comment));
443
+ }
444
+ if (c.replicaIdentity)
445
+ before.push(`ALTER TABLE ${c.table} REPLICA IDENTITY USING INDEX ${quoteIdent(c.target)}`);
446
+ if (c.clustered)
447
+ before.push(`ALTER TABLE ${c.table} CLUSTER ON ${quoteIdent(c.target)}`);
448
+ }
449
+ for (const v of [...views].sort((a, b) => a.depth - b.depth)) {
450
+ for (const s of createSteps(v.declared, marker))
451
+ after.push(s.sql);
452
+ after.push(`ALTER VIEW ${v.ident} OWNER TO ${quoteIdent(v.owner)}`);
453
+ for (const g of v.grants)
454
+ after.push(`GRANT ${g.privilege} ON TABLE ${v.ident} TO ${g.grantee}${g.grantable ? " WITH GRANT OPTION" : ""}`);
455
+ }
456
+ return { before, after };
457
+ }
458
+ /** What the gate binds about the carried objects: each one's kind, names and definition, and each view. */
459
+ export function carriedSubject(d) {
460
+ return {
461
+ ...(d.sequence ? { sequence: { kind: d.sequence.kind, name: `${d.sequence.schema}.${d.sequence.name}` } } : {}),
462
+ carried: d.carried.map((c) => ({ kind: c.kind, table: c.tableName, name: c.name, target: c.target, definition: c.definition, ...(c.primary !== undefined ? { primary: c.primary } : {}) })),
463
+ views: d.views.map((v) => ({ name: v.name, ddl: v.declared.ddl })),
464
+ };
465
+ }
466
+ /** Whether a declared column is NOT NULL once it is switched: declared so, an identity column, or part of a carried primary key. */
467
+ export const requiresNotNull = (column, carried) => column.notNull === true || column.generated?.kind === "identity" || SERIAL_TYPES[column.type ?? ""] !== undefined || carried.some((c) => c.kind === "key" && c.primary === true);
468
+ /** The integer type of each `serial` spelling: what the column holds, and the sequence's type. */
469
+ export const SERIAL_TYPES = {
470
+ smallserial: "smallint",
471
+ serial2: "smallint",
472
+ serial: "integer",
473
+ serial4: "integer",
474
+ bigserial: "bigint",
475
+ serial8: "bigint",
476
+ };
477
+ /** A declared column's type as the server holds it: a `serial` spelling is its integer type. */
478
+ export const storedType = (type) => (type === undefined ? undefined : (SERIAL_TYPES[type.replace(/^(public|pg_catalog)\./, "")] ?? type));
479
+ /**
480
+ * The working copies of the carried objects on the server, by working name.
481
+ * One under a working name that is somebody else's is refused, as for the
482
+ * other working objects (`./observe.ts`).
483
+ */
484
+ export async function carriedStates(client, carried, ours, refuse) {
485
+ const out = new Map();
486
+ const indexes = carried.filter((c) => c.kind === "index" || c.kind === "key");
487
+ const constraints = carried.filter((c) => c.kind === "check" || c.kind === "foreign-key");
488
+ if (indexes.length > 0) {
489
+ const rows = await client.query(`SELECT c.relname AS name, i.indisvalid AS valid, pg_catalog.obj_description(c.oid, 'pg_class') AS comment, pg_catalog.pg_get_indexdef(c.oid) AS def
490
+ FROM ROWS FROM (pg_catalog.unnest($1::text[]), pg_catalog.unnest($2::text[])) AS x(name, schema)
491
+ JOIN pg_catalog.pg_namespace s ON s.nspname = x.schema JOIN pg_catalog.pg_class c ON c.relnamespace = s.oid AND c.relname = x.name
492
+ LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid`, [indexes.map((c) => c.working), indexes.map((c) => c.schema)]);
493
+ for (const r of rows) {
494
+ const c = indexes.find((x) => x.working === r.name);
495
+ const owner = ours(r.comment ?? undefined);
496
+ const expected = c.definition.replace(/ INDEX CONCURRENTLY /, " INDEX ");
497
+ if (owner === "theirs" || (owner === "none" && r.def !== expected))
498
+ refuse(`index ${c.schema}.${c.working}`);
499
+ out.set(c.working, { present: true, ready: r.valid === true, marked: owner === "ours" });
500
+ }
501
+ }
502
+ if (constraints.length > 0) {
503
+ const rows = await client.query(`SELECT k.conname AS name, k.conrelid::text AS rel, k.convalidated AS validated, pg_catalog.obj_description(k.oid, 'pg_constraint') AS comment
504
+ FROM ROWS FROM (pg_catalog.unnest($1::text[]), pg_catalog.unnest($2::oid[])) AS x(name, rel) JOIN pg_catalog.pg_constraint k ON k.conrelid = x.rel AND k.conname = x.name`, [constraints.map((c) => c.working), constraints.map((c) => c.tableOid)]);
505
+ for (const r of rows) {
506
+ const c = constraints.find((x) => x.working === r.name && x.tableOid === r.rel);
507
+ if (ours(r.comment ?? undefined) !== "ours")
508
+ refuse(`constraint ${c.working} on ${c.tableName}`);
509
+ out.set(c.working, { present: true, ready: r.validated === true || !c.validated, marked: true });
510
+ }
511
+ }
512
+ return out;
513
+ }
514
+ /** The carried objects whose working copy is not there, not marked, or not yet valid. */
515
+ export const carriedNotReady = (carried, states) => carried.filter((c) => {
516
+ const s = states.get(c.working);
517
+ return !s || !s.ready || !s.marked;
518
+ });