@intentius/chant-lexicon-sql 0.124.30 → 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,28 @@
1
+ /**
2
+ * `ReplacingTable`: a ReplacingMergeTree table with a version column.
3
+ *
4
+ * ReplacingMergeTree keeps one row per sort key. When a merge meets rows with
5
+ * the same key it keeps the one with the highest version, so writing a row
6
+ * again with a higher version is how an update is expressed. Until a merge
7
+ * runs, a query sees every version; read with `FINAL`, or aggregate with
8
+ * `argMax`, when the latest row is what you want.
9
+ *
10
+ * Every field is built from a parameter or fixed by the template, inside the
11
+ * subset `chant build` interprets, so a drift on the table is reported against
12
+ * the parameter that produced the field when the composite is interpreted.
13
+ */
14
+ import { Composite } from "@intentius/chant/composite";
15
+ import { table } from "../clickhouse/entities.js";
16
+ /** A ReplacingMergeTree table that keeps the highest version of each row, with its version column. */
17
+ export const ReplacingTable = Composite((props) => {
18
+ const version = props.version ?? "version";
19
+ const replacing = table `
20
+ CREATE TABLE ${props.name} (
21
+ ${props.columns},
22
+ ${version} ${props.versionType ?? "UInt64"}
23
+ )
24
+ ENGINE = ReplacingMergeTree(${version})
25
+ ${props.partitionBy ? `PARTITION BY ${props.partitionBy}` : ""}
26
+ ORDER BY ${props.orderBy}`;
27
+ return { table: replacing };
28
+ }, "ReplacingTable");
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `RollupView`: a materialized view that rolls a source table up into its own target table.
3
+ *
4
+ * The target is a SummingMergeTree (or another engine named by `engine`)
5
+ * sorted by the rollup's grouping key. The materialized view writes into it
6
+ * with `TO`, so the target is an ordinary table that can be queried, altered
7
+ * and backfilled on its own, and dropping the view leaves the data. Every
8
+ * column the select list writes has to be one the target declares
9
+ * (SQLCH110), so alias each select item to a target column.
10
+ *
11
+ * The view reads `source` by reference, so it depends on the source and on
12
+ * its target, and the build orders all three.
13
+ */
14
+ import { Composite } from "@intentius/chant/composite";
15
+ import { table, view } from "../clickhouse/entities.js";
16
+ /** A materialized view that rolls a source table up into a target table it declares beside it. */
17
+ export const RollupView = Composite((props) => {
18
+ const target = table `
19
+ CREATE TABLE ${props.name} (${props.columns})
20
+ ENGINE = ${props.engine ?? "SummingMergeTree"}
21
+ ${props.partitionBy ? `PARTITION BY ${props.partitionBy}` : ""}
22
+ ORDER BY ${props.orderBy ?? `(${props.groupBy})`}`;
23
+ const rollup = view `
24
+ CREATE MATERIALIZED VIEW ${`${props.name}_mv`} TO ${target} AS
25
+ SELECT ${props.select}
26
+ FROM ${props.source}
27
+ GROUP BY ${props.groupBy}`;
28
+ return { table: target, view: rollup };
29
+ }, "RollupView");
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `ShardedTable`: a ReplicatedMergeTree table on every node of a cluster and the Distributed table that spreads reads and writes across it.
3
+ *
4
+ * Both tables are created `ON CLUSTER`. The local table, `<name>_local`, holds
5
+ * each shard's rows and replicates them within the shard; with no engine
6
+ * arguments it takes the server's default replication path. The Distributed
7
+ * table, `<name>`, holds no data: an insert goes to one shard by the sharding
8
+ * key, and a query runs on every shard and merges the results.
9
+ *
10
+ * The Distributed table reads the local one by reference, so the build creates
11
+ * the local table first. Give `name` without a database: the Distributed
12
+ * engine names its table in the current database.
13
+ */
14
+ import { Composite } from "@intentius/chant/composite";
15
+ import { table } from "../clickhouse/entities.js";
16
+ /** A ReplicatedMergeTree table on every node of a cluster and the Distributed table over it. */
17
+ export const ShardedTable = Composite((props) => {
18
+ const local = table `
19
+ CREATE TABLE ${`${props.name}_local`} ON CLUSTER ${props.cluster} (${props.columns})
20
+ ENGINE = ReplicatedMergeTree
21
+ ${props.partitionBy ? `PARTITION BY ${props.partitionBy}` : ""}
22
+ ORDER BY ${props.orderBy}`;
23
+ const distributed = table `
24
+ CREATE TABLE ${props.name} ON CLUSTER ${props.cluster} (${props.columns})
25
+ ENGINE = Distributed(${props.cluster}, currentDatabase(), ${local}, ${props.shardingKey ?? "rand()"})`;
26
+ return { local, distributed };
27
+ }, "ShardedTable");
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `SoftDeleteTable`: a table whose rows carry created and updated timestamps and are deleted by marking them.
3
+ *
4
+ * The composite appends `created_at` and `updated_at` (both `timestamptz NOT
5
+ * NULL DEFAULT now()`) and a nullable `deleted_at` to the columns it is
6
+ * given. A row is live while `deleted_at` is null. The `<name>_live` view
7
+ * reads the live rows (it sets `security_invoker`, so row-level security on
8
+ * the table applies to whoever reads the view), and a partial index on the
9
+ * live rows keeps lookups by the key the app uses off the dead ones.
10
+ *
11
+ * Postgres has no column default that follows an update: the app, or a
12
+ * trigger it owns, sets `updated_at`.
13
+ */
14
+ import { Composite } from "@intentius/chant/composite";
15
+ import { index, literal, table, view } from "../postgres/entities.js";
16
+ /** A table with created and updated timestamps and a soft-delete column, a view of its live rows and an index over them. */
17
+ export const SoftDeleteTable = Composite((props) => {
18
+ const schema = props.schema ?? "public";
19
+ const deleted = props.deletedAt ?? "deleted_at";
20
+ const rows = table `
21
+ CREATE TABLE ${schema}.${props.name} (
22
+ ${props.columns},
23
+ ${props.createdAt ?? "created_at"} timestamptz NOT NULL DEFAULT now(),
24
+ ${props.updatedAt ?? "updated_at"} timestamptz NOT NULL DEFAULT now(),
25
+ ${deleted} timestamptz
26
+ );
27
+ COMMENT ON TABLE ${schema}.${props.name} IS ${literal(props.comment ?? "Soft-deleted: a row is live while its deletion timestamp is null")}`;
28
+ const live = view `
29
+ CREATE VIEW ${schema}.${`${props.name}_live`} WITH (security_invoker = true) AS
30
+ SELECT * FROM ${rows} WHERE ${deleted} IS NULL`;
31
+ const liveIndex = index `
32
+ CREATE INDEX ${`${props.name}_live_idx`} ON ${rows} (${props.liveKey ?? "id"}) WHERE ${deleted} IS NULL`;
33
+ return { table: rows, live, liveIndex };
34
+ }, "SoftDeleteTable");
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `TenantTable`: a multi-tenant table whose tenant key leads every key and index.
3
+ *
4
+ * The composite adds the tenant column (`NOT NULL`) in front of the columns it
5
+ * is given, makes the primary key `(<tenant>, <primaryKey>)` and creates one
6
+ * index on `(<tenant>, <indexOn>)`. With the tenant first in each, a query
7
+ * scoped to one tenant reads one contiguous range of every index, and no index
8
+ * serves a cross-tenant scan by accident. Row-level security is separate: a
9
+ * policy on the table is what keeps one tenant's role out of another's rows.
10
+ */
11
+ import { Composite } from "@intentius/chant/composite";
12
+ import { index, table } from "../postgres/entities.js";
13
+ /** A multi-tenant table with the tenant key first in its primary key and its index. */
14
+ export const TenantTable = Composite((props) => {
15
+ const schema = props.schema ?? "public";
16
+ const tenant = props.tenant ?? "tenant_id";
17
+ const rows = table `
18
+ CREATE TABLE ${schema}.${props.name} (
19
+ ${tenant} ${props.tenantType ?? "uuid"} NOT NULL,
20
+ ${props.columns},
21
+ PRIMARY KEY (${tenant}, ${props.primaryKey})
22
+ )`;
23
+ const byTenant = index `
24
+ CREATE INDEX ${`${props.name}_tenant_idx`} ON ${rows} (${tenant}, ${props.indexOn})`;
25
+ return { table: rows, index: byTenant };
26
+ }, "TenantTable");
@@ -0,0 +1,8 @@
1
+ /**
2
+ * chant#3787 — the call signature a composite is typed with, so a member's
3
+ * `.columns` are the generated `SqlSchema` entry for the name its props give
4
+ * it. `users.table.columns.emial` then fails tsc as `table<"app.users">`'s
5
+ * would. The definition is unchanged at run time: `Composite`'s third type
6
+ * argument carries this signature in front of the plain one.
7
+ */
8
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,206 @@
1
+ /**
2
+ * The `sql` namespace in `chant.config.ts`.
3
+ *
4
+ * ```ts
5
+ * import type { ChantConfig } from "@intentius/chant/config";
6
+ * import "@intentius/chant-lexicon-sql"; // brings the `sql` key into ChantConfig
7
+ *
8
+ * export default {
9
+ * lexicons: ["sql"],
10
+ * sql: {
11
+ * dialect: "clickhouse",
12
+ * profiles: {
13
+ * staging: { url: "http://clickhouse.staging:8123", user: { env: "CH_USER" }, password: { env: "CH_PASSWORD" } },
14
+ * prod: { url: "https://clickhouse.example.com:8443", user: { env: "CH_USER" }, password: { env: "CH_PROD_PASSWORD" }, databases: ["analytics"] },
15
+ * },
16
+ * },
17
+ * } satisfies ChantConfig;
18
+ * ```
19
+ *
20
+ * A profile is keyed by chant environment, as `grafana.profiles.<env>` is:
21
+ * the server `chant import --from <env>`, `chant lifecycle diff <env> --live`
22
+ * and `plan` read. Credentials are named by their environment variable, never
23
+ * written in the config, or minted as a short-lived token (`password:
24
+ * { token: "rds-iam" }`; `./token-source.ts`). An environment with no profile falls back to
25
+ * `CLICKHOUSE_URL`, `CLICKHOUSE_USER` and `CLICKHOUSE_PASSWORD`, or for
26
+ * Postgres `POSTGRES_URL`, `POSTGRES_USER` and `POSTGRES_PASSWORD`. A profile
27
+ * whose `url` is `postgres://` or `postgresql://` is a Postgres server.
28
+ *
29
+ * `dialect` names the database a project's schema is for. Declarations carry
30
+ * their dialect in their own type (a ClickHouse table is `ClickHouse::Table`),
31
+ * so the setting is for the things a declaration cannot answer: which dialect
32
+ * a live environment is read as on import, and which dialect's checks run on
33
+ * output that names none. It defaults to `clickhouse`, the first dialect.
34
+ * `postgres` is the second (#3289). A list (`dialect: ["clickhouse",
35
+ * "postgres"]`) is accepted for a workspace with members of more than one
36
+ * dialect (#3047 question 1); one build still holds one dialect.
37
+ *
38
+ * `provider` (#3282) names the managed Postgres service, on the namespace or
39
+ * on one profile: `sql: { dialect: "postgres", provider: "rds" }`, or
40
+ * `profiles: { prod: { url, provider: "aurora" } }`. A provider's data lives
41
+ * in `@intentius/chant-lexicon-sql/postgres` (`providerData`).
42
+ */
43
+ import { z } from "zod";
44
+ import { PLANNED_SQL_DIALECTS, SQL_DIALECTS } from "./dialects.js";
45
+ import { POSTGRES_PROVIDERS } from "./postgres/providers/types.js";
46
+ import { POSTGRES_MAJORS } from "./spec/postgres-pin.js";
47
+ import { parseTopology } from "./clickhouse/topology.js";
48
+ import { tokenSourceSchema } from "./token-source.js";
49
+ const envRef = z.strictObject({ env: z.string() });
50
+ /** The managed Postgres service an environment runs on (#3282). */
51
+ const providerName = z.enum(POSTGRES_PROVIDERS, {
52
+ error: () => `expected a Postgres provider (${POSTGRES_PROVIDERS.join(", ")})`,
53
+ });
54
+ /** A ClickHouse topology, in its string form or its object form (`./clickhouse/topology.ts`). */
55
+ const topologySchema = z.union([
56
+ z.string().refine((v) => {
57
+ try {
58
+ parseTopology(v);
59
+ return true;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }, { error: "expected single, cluster:<name>, replicated, replicated:<cluster> or cloud" }),
65
+ z.discriminatedUnion("kind", [
66
+ z.strictObject({ kind: z.literal("single") }),
67
+ z.strictObject({ kind: z.literal("cluster"), cluster: z.string().min(1), replicaPath: z.string().optional(), replicaName: z.string().optional() }),
68
+ z.strictObject({ kind: z.literal("replicated"), cluster: z.string().min(1).optional() }),
69
+ z.strictObject({ kind: z.literal("cloud") }),
70
+ ]),
71
+ ]);
72
+ export const sqlProfileSchema = z.strictObject({
73
+ /**
74
+ * Where the server is. ClickHouse: the base URL of its HTTP interface
75
+ * (`http://clickhouse:8123`). Postgres: a connection URL
76
+ * (`postgres://db.internal:5432/shop`), with no password in it; the scheme
77
+ * is what makes a profile a Postgres one.
78
+ */
79
+ url: z.string(),
80
+ /** The user, named by its environment variable. `default` when omitted. */
81
+ user: envRef.optional(),
82
+ /**
83
+ * The password, named by its environment variable, or a token source that
84
+ * mints a short-lived one when a connection needs it (#3685):
85
+ * `{ token: "rds-iam" }`, `{ token: "cloud-sql-iam" }`, `{ token: "entra" }`
86
+ * (Postgres), or `{ token: "command", command: ["./mint.sh"] }`, whose
87
+ * standard output is the password. A token is minted again before it
88
+ * expires. See `./token-source.ts`.
89
+ */
90
+ password: z.union([envRef, tokenSourceSchema]).optional(),
91
+ /**
92
+ * The databases this environment's schema lives in. Import reads these and
93
+ * nothing else; when omitted, every database except the server's own
94
+ * (`system`, `information_schema`, `INFORMATION_SCHEMA`).
95
+ */
96
+ databases: z.array(z.string()).optional(),
97
+ /** The database an unqualified declaration is created in. `default` when omitted. */
98
+ defaultDatabase: z.string().optional(),
99
+ /**
100
+ * ClickHouse import (#3718): the SQL user-defined functions `chant import
101
+ * --from <env>` adopts besides those the imported objects use, by name, or
102
+ * by prefix with a trailing `*` (`shop_*`). A function belongs to no
103
+ * database, so without this only the functions the imported tables, views
104
+ * and dictionaries use (and the functions those call) are imported. A use
105
+ * is found by name or, since ClickHouse stores a call as the function's
106
+ * body, by that body (#3745).
107
+ */
108
+ importFunctions: z.array(z.string()).optional(),
109
+ /**
110
+ * ClickHouse: the topology this environment runs, which the applier, the
111
+ * plan and the rebuild migration render every statement for: `single`,
112
+ * `cluster:<name>` (`ON CLUSTER`, `Replicated*MergeTree` with a Keeper
113
+ * path), `replicated` (a `Replicated` database; `replicated:<cluster>`
114
+ * creates the databases on that cluster) or `cloud`. The object form sets
115
+ * a cluster's Keeper path and replica name:
116
+ * `{ kind: "cluster", cluster: "main", replicaPath, replicaName }`. When
117
+ * omitted, statements are sent as declared.
118
+ */
119
+ topology: topologySchema.optional(),
120
+ /**
121
+ * Postgres: the schemas this environment's schema lives in. Import and
122
+ * observation read these and nothing else; when omitted, every schema except
123
+ * the server's own (`pg_catalog`, `information_schema`, `pg_toast`, the temp
124
+ * schemas).
125
+ */
126
+ schemas: z.array(z.string()).optional(),
127
+ /** Postgres: the schema an unqualified declaration is created in. `public` when omitted. */
128
+ defaultSchema: z.string().optional(),
129
+ /**
130
+ * Postgres: the schema `effect()` receipts are kept in, as
131
+ * `<schema>.__chant_receipts`. When omitted they are in
132
+ * `chant_receipts.receipts`, a schema chant creates; name an existing schema
133
+ * on a server where the role cannot `CREATE SCHEMA`.
134
+ */
135
+ receiptsSchema: z.string().optional(),
136
+ /**
137
+ * The managed service this environment's Postgres runs on, when it differs
138
+ * from `sql.provider`. Import reads the provider's own roles, schemas and
139
+ * extensions as foreign.
140
+ */
141
+ provider: providerName.optional(),
142
+ /**
143
+ * Postgres apply: how long one statement waits for its lock before it
144
+ * fails (`lock_timeout`), in milliseconds, so a change blocked behind a
145
+ * long query fails fast instead of queueing every query behind it. The
146
+ * catalog reads (`chant lifecycle diff --live`, `chant sql plan`, the
147
+ * import, and an apply's read before its first statement) wait under it
148
+ * too, and name the session holding the lock when it passes. 5000 when
149
+ * omitted.
150
+ */
151
+ lockTimeoutMs: z.number().int().nonnegative().optional(),
152
+ /**
153
+ * Postgres apply: the `statement_timeout` for a statement that changes only
154
+ * the catalog, in milliseconds. 60000 when omitted.
155
+ */
156
+ statementTimeoutMs: z.number().int().nonnegative().optional(),
157
+ /**
158
+ * Postgres apply: the `statement_timeout` for a statement that reads or
159
+ * rewrites every row (a table rewrite, a constraint validation, a
160
+ * `CONCURRENTLY` index build), in milliseconds. 0, no limit, when omitted.
161
+ */
162
+ scanTimeoutMs: z.number().int().nonnegative().optional(),
163
+ /**
164
+ * Whether chant manages access in this environment. Postgres (#3681): the
165
+ * declared row-level security policies and each table's row-level security,
166
+ * the declared roles (created when missing, never dropped), and the
167
+ * privileges on the declared objects and the default privileges, which a
168
+ * plan compares and an apply grants and revokes. ClickHouse (#3716): the
169
+ * declared users, roles, row policies and grants. Off when omitted: the
170
+ * access declarations are not planned or applied, and the server's access
171
+ * is not read, so a project that manages access elsewhere is not affected.
172
+ */
173
+ access: z.boolean().optional(),
174
+ });
175
+ const dialectName = z.enum(SQL_DIALECTS);
176
+ /** Why a `dialect` value is refused: a planned dialect is "not implemented yet", anything else unknown. */
177
+ function dialectError(input) {
178
+ const names = Array.isArray(input) ? input : [input];
179
+ const planned = names.find((n) => typeof n === "string" && PLANNED_SQL_DIALECTS.includes(n));
180
+ if (planned !== undefined) {
181
+ return `the ${planned} dialect is not implemented yet; the implemented dialects are ${SQL_DIALECTS.join(", ")}`;
182
+ }
183
+ return `expected a dialect (${SQL_DIALECTS.join(", ")}) or a non-empty list of them`;
184
+ }
185
+ export const sqlConfigSchema = z.strictObject({
186
+ /** The dialect, or the dialects, the project's schema is for. */
187
+ dialect: z.union([dialectName, z.array(dialectName).min(1)], { error: (iss) => dialectError(iss.input) }).optional(),
188
+ /**
189
+ * The managed Postgres service the project deploys to (`rds`, `aurora`,
190
+ * `cloud-sql`, `azure`, `neon`, `supabase`). SQLPG004 reads it to refuse an
191
+ * extension the provider does not allow. Omit it for a self-hosted server.
192
+ */
193
+ provider: providerName.optional(),
194
+ /**
195
+ * The Postgres major the project targets. The editor completes and hovers
196
+ * only what that major has (a function added in 18 is not offered at 16);
197
+ * without it, the newest supported major is used.
198
+ */
199
+ postgresMajor: z
200
+ .number()
201
+ .int()
202
+ .refine((n) => POSTGRES_MAJORS.includes(n), { error: `expected a supported Postgres major (${POSTGRES_MAJORS.join(", ")})` })
203
+ .optional(),
204
+ /** One server per chant environment. */
205
+ profiles: z.record(z.string(), sqlProfileSchema).optional(),
206
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The applier's plumbing, the same in every dialect (lexicon-authoring/
3
+ * applying.mdx): reading a build's output, the outcome's tri-state (applied,
4
+ * not attempted, and failed for a statement the server refused), its
5
+ * projection onto core's apply envelope, and the ownership marker an apply
6
+ * stamps and prunes by.
7
+ *
8
+ * What a dialect's applier decides for itself: the statements a change takes,
9
+ * which changes it refuses and why, how it waits for the server, and how a
10
+ * prune drops an object.
11
+ */
12
+ import { applyResult, notAttemptedAll, } from "@intentius/chant/apply";
13
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
14
+ /**
15
+ * The objects a `chant build` output declares for `dialect`, in creation
16
+ * order, each passed through `map`. Takes the sql lexicon's primary output,
17
+ * or a multi-lexicon output holding it under `sql`.
18
+ */
19
+ export function readBuildObjects(json, dialect, map) {
20
+ const raw = JSON.parse(json);
21
+ const doc = isObject(raw) && isObject(raw.sql) ? raw.sql : raw;
22
+ if (!isObject(doc) || doc.dialect !== dialect || !Array.isArray(doc.objects)) {
23
+ throw new Error(`not a sql lexicon build output: expected { dialect: "${dialect}", objects: [...] }`);
24
+ }
25
+ return doc.objects.map((o, i) => {
26
+ if (!isObject(o) || typeof o.export !== "string" || typeof o.ddl !== "string" || typeof o.type !== "string") {
27
+ throw new Error(`sql build output: objects[${i}] has no export, type or ddl`);
28
+ }
29
+ const dependsOn = [...new Set((Array.isArray(o.dependsOn) ? o.dependsOn : []).filter((d) => typeof d === "string").map((d) => d.split(".")[0]))];
30
+ return map({ export: o.export, type: o.type, ddl: o.ddl, dependsOn }, i);
31
+ });
32
+ }
33
+ /**
34
+ * An apply in which the server refused a statement. The envelope has no
35
+ * bucket for "attempted and not converged", so the applier finishes the rest
36
+ * and throws this, carrying the outcome.
37
+ */
38
+ export class SqlApplyError extends Error {
39
+ constructor(
40
+ /** The dialect as a message names it: `ClickHouse`. */
41
+ dialect, outcome) {
42
+ super(`${dialect} apply to ${outcome.target}: ${outcome.failed.length} object(s) failed ` +
43
+ `(${outcome.failed.map((f) => `${f.kind}/${f.name}: ${f.error}`).join("; ")}); ` +
44
+ `${outcome.applied.length} applied, ${outcome.pruned.length} pruned, ${outcome.notAttempted.length} not attempted before and after`);
45
+ this.outcome = outcome;
46
+ this.name = "SqlApplyError";
47
+ }
48
+ }
49
+ /** An outcome in which nothing was attempted: every object not attempted with one reason. */
50
+ export function notAttemptedOutcome(refs, reason, detail) {
51
+ return { target: "", source: "", applied: [], pruned: [], notAttempted: notAttemptedAll(refs, reason, detail), failed: [] };
52
+ }
53
+ /**
54
+ * Project an apply outcome onto core's apply envelope. The outcome keeps what
55
+ * the envelope has no room for: the statements each object took and the
56
+ * server they went to.
57
+ */
58
+ export function toApplyResult(outcome) {
59
+ return applyResult(outcome.applied.map((a) => ({ kind: a.kind, name: a.name, action: a.action, ...(a.physicalId ? { physicalId: a.physicalId } : {}) })), outcome.pruned.map((p) => ({ kind: p.kind, name: p.name, deleted: p.deleted })), outcome.notAttempted.map((n) => ({ kind: n.kind, name: n.name, reason: n.reason, ...(n.detail ? { detail: n.detail } : {}) })));
60
+ }
61
+ /**
62
+ * The marker to stamp and prune by: `stack` and `ownershipEnv` when passed,
63
+ * else the project's `ownership` config. Undefined without a stack. `who`
64
+ * names the caller in the error for an env an apply cannot resolve.
65
+ */
66
+ export function resolveOwnershipMarker(args, config, who) {
67
+ const o = config?.ownership;
68
+ const configured = o && o.enabled !== false ? o : undefined;
69
+ const stack = args.stack ?? configured?.stack;
70
+ if (!stack)
71
+ return undefined;
72
+ const env = args.ownershipEnv ?? (typeof configured?.env === "string" ? configured.env : undefined);
73
+ if (env === undefined && configured?.env !== undefined) {
74
+ throw new Error(`${who}: ownership.env is a build parameter reference, which an apply cannot resolve; pass ownershipEnv`);
75
+ }
76
+ return { stack, ...(env ? { env } : {}) };
77
+ }
78
+ /** Changes grouped by the object they change, in order. */
79
+ export function changesByObject(changes) {
80
+ const byObject = new Map();
81
+ for (const c of changes)
82
+ byObject.set(c.object, [...(byObject.get(c.object) ?? []), c]);
83
+ return byObject;
84
+ }
85
+ /**
86
+ * The declared objects `dependsOn` names that are not on the server: declared
87
+ * in this build, and not created or found in this apply. An object with any
88
+ * is not attempted, `dependency-failed`, with {@link dependencyFailedDetail}.
89
+ */
90
+ export function missingDependencies(self, dependsOn, declared, onServer) {
91
+ return dependsOn.filter((d) => declared.has(d) && d !== self && !onServer.has(d));
92
+ }
93
+ export const dependencyFailedDetail = (missing) => `references ${missing.join(", ")}, which is not on the server: it failed or was not attempted in this apply`;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The change classifier's framework, the same in every dialect: the shape of
3
+ * a rule, a classified change, `classifyDisruption()` over a dialect's rules,
4
+ * and the text report.
5
+ *
6
+ * The classes are data, one set per dialect. ClickHouse classes a change by
7
+ * whether `ALTER` can make it (metadata, background rewrite, rebuild); another
8
+ * dialect classes by the lock a change takes and whether it rewrites the
9
+ * table. Each dialect declares its classes with {@link ChangeClasses} and its
10
+ * rules with {@link classifierRule}, every rule citing the database's own
11
+ * documentation for the restriction behind it.
12
+ */
13
+ export const classifierRule = (id, cls, title, restriction, cite) => ({
14
+ id,
15
+ class: cls,
16
+ title,
17
+ restriction,
18
+ cite,
19
+ });
20
+ /** A classified change from `before` to `after`, its class read from `rules`. */
21
+ export function classifiedChange(rules, object, field, rule, before, after, extra = {}) {
22
+ return {
23
+ object,
24
+ field,
25
+ ...(before !== undefined ? { before: String(before) } : {}),
26
+ ...(after !== undefined ? { after: String(after) } : {}),
27
+ rule,
28
+ class: rules[rule].class,
29
+ ...extra,
30
+ };
31
+ }
32
+ // ── classifyDisruption() ────────────────────────────────────────────────
33
+ const RANK = { "in-place": 0, rolling: 1, replace: 2, destroy: 3, unknown: 4 };
34
+ /**
35
+ * `classifyDisruption()` over a dialect's rules: each update's disruption is
36
+ * the worst class among its changed paths, and `unknown` when any path cannot
37
+ * be classified from the path alone.
38
+ */
39
+ export function classifyDisruptionWith(classifier, options) {
40
+ const out = {};
41
+ for (const q of options.changes) {
42
+ if (q.type !== undefined && !q.type.startsWith(classifier.typePrefix))
43
+ continue;
44
+ let level = "in-place";
45
+ const because = [];
46
+ const rules = new Set();
47
+ let ambiguous = false;
48
+ for (const d of q.deltas) {
49
+ const r = classifier.ruleFor(d.path);
50
+ if (r === undefined || r === "ambiguous") {
51
+ ambiguous = true;
52
+ because.push(d.path);
53
+ continue;
54
+ }
55
+ const l = classifier.classes.disruption[classifier.rules[r].class];
56
+ if (RANK[l] > RANK[level]) {
57
+ level = l;
58
+ because.length = 0;
59
+ }
60
+ if (RANK[l] === RANK[level])
61
+ because.push(d.path);
62
+ rules.add(r);
63
+ }
64
+ if (ambiguous) {
65
+ out[q.name] = { disruption: "unknown", because, detail: classifier.ambiguousDetail };
66
+ continue;
67
+ }
68
+ out[q.name] = {
69
+ disruption: level,
70
+ because,
71
+ detail: [...rules].map((r) => `${r} ${classifier.rules[r].title}`).join("; "),
72
+ };
73
+ }
74
+ return out;
75
+ }
76
+ // ── The report ──────────────────────────────────────────────────────────
77
+ /**
78
+ * A change set as text, for a terminal or a pull request: each object's
79
+ * changes with their class, rule, restriction and citation, then a count per
80
+ * class and the hints. `trailer` is the dialect's closing lines (what a plan
81
+ * refuses and what to run instead); it is left out when nothing changed.
82
+ */
83
+ export function renderChangeSet(diff, opts) {
84
+ const lines = [];
85
+ if (opts.title)
86
+ lines.push(opts.title, "");
87
+ if (diff.changes.length === 0) {
88
+ lines.push("No changes.");
89
+ return lines.join("\n");
90
+ }
91
+ const byObject = new Map();
92
+ for (const c of diff.changes)
93
+ byObject.set(c.object, [...(byObject.get(c.object) ?? []), c]);
94
+ for (const [object, changes] of byObject) {
95
+ lines.push(object);
96
+ for (const c of changes) {
97
+ const rule = opts.rules[c.rule];
98
+ const values = c.before !== undefined && c.after !== undefined ? `: ${c.before} -> ${c.after}` : c.after !== undefined ? `: ${c.after}` : c.before !== undefined ? `: ${c.before}` : "";
99
+ lines.push(` [${opts.classes.label[c.class]}] ${c.field}${values}`);
100
+ lines.push(` ${c.rule} ${rule.title}${c.destructive ? " (destroys data)" : ""}. ${rule.restriction} ${rule.cite}`);
101
+ if (c.note)
102
+ lines.push(` ${c.note}`);
103
+ }
104
+ }
105
+ const counts = opts.classes.order
106
+ .map((k) => [k, diff.changes.filter((c) => c.class === k).length])
107
+ .filter(([, n]) => n > 0)
108
+ .map(([k, n]) => `${n} ${opts.classes.label[k]}`);
109
+ lines.push("", counts.join(", "));
110
+ for (const h of diff.hints)
111
+ lines.push(`hint: ${h}`);
112
+ if (opts.trailer)
113
+ lines.push(...opts.trailer);
114
+ return lines.join("\n");
115
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * A throwaway database server in a container, for every dialect: started on a
3
+ * random loopback port under a unique name, waited on until it answers, and
4
+ * always removed.
5
+ *
6
+ * Generation uses it on a pin move to read the pinned server's catalog, and
7
+ * the live tests use it so every container chant starts is started and
8
+ * cleaned up one way. What "answers" means (an HTTP ping, a query) and the
9
+ * image's environment are the dialect's.
10
+ */
11
+ import { execFile } from "node:child_process";
12
+ import { promisify } from "node:util";
13
+ import { randomBytes } from "node:crypto";
14
+ const exec = promisify(execFile);
15
+ /** True when a Docker daemon answers. Tests that need a server skip when this is false. */
16
+ export async function dockerAvailable() {
17
+ try {
18
+ await exec("docker", ["info", "--format", "{{.ServerVersion}}"], { timeout: 15000 });
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ /** A container name no other run picks: `<prefix>-<pid>-<random>`. */
26
+ export function uniqueContainerName(prefix) {
27
+ return `${prefix}-${process.pid}-${randomBytes(4).toString("hex")}`;
28
+ }
29
+ /** The loopback host port Docker published for `containerPort` on `name`. */
30
+ export async function publishedPort(name, containerPort) {
31
+ const { stdout } = await exec("docker", ["port", name, `${containerPort}/tcp`]);
32
+ const port = stdout.split("\n")[0]?.trim().split(":").pop();
33
+ if (!port)
34
+ throw new Error(`docker port reported no host port for ${name}`);
35
+ return port;
36
+ }
37
+ /**
38
+ * Run `image` detached, publish `containerPort` on a random loopback port,
39
+ * and wait until `ready(port)` is true, polling every half second. Pulls the
40
+ * image when it is not present, which is the one network step here. On any
41
+ * failure the container is removed before the error is thrown.
42
+ */
43
+ export async function startScratchContainer(options) {
44
+ const name = uniqueContainerName(options.namePrefix);
45
+ let removed = false;
46
+ const stop = async () => {
47
+ if (removed)
48
+ return;
49
+ removed = true;
50
+ await exec("docker", ["rm", "-f", ...(options.removeVolumes ? ["-v"] : []), name]).catch(() => undefined);
51
+ };
52
+ try {
53
+ const env = Object.entries(options.env ?? {}).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
54
+ await exec("docker", ["run", "-d", "--name", name, "-p", `127.0.0.1::${options.containerPort}`, ...env, options.image], { timeout: 600000 });
55
+ const port = await publishedPort(name, options.containerPort);
56
+ options.onStarted?.(name);
57
+ const deadline = Date.now() + options.readyTimeoutMs;
58
+ while (!(await options.ready(port))) {
59
+ if (Date.now() > deadline)
60
+ throw new Error(`${name} ${options.notReady} within the timeout`);
61
+ await new Promise((r) => setTimeout(r, 500));
62
+ }
63
+ return { name, port, stop };
64
+ }
65
+ catch (err) {
66
+ await stop();
67
+ throw err;
68
+ }
69
+ }