@intentius/chant-lexicon-sql 0.124.46 → 0.124.49

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 (402) hide show
  1. package/dist/change-set.d.ts +1 -1
  2. package/dist/change-set.d.ts.map +1 -1
  3. package/dist/change-set.js +2 -1
  4. package/dist/clickhouse/apply/statements.d.ts +9 -0
  5. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  6. package/dist/clickhouse/apply/statements.js +24 -0
  7. package/dist/clickhouse/import/live-export.d.ts +7 -3
  8. package/dist/clickhouse/import/live-export.d.ts.map +1 -1
  9. package/dist/clickhouse/import/live-export.js +21 -5
  10. package/dist/clickhouse/live/named-collections.d.ts +27 -0
  11. package/dist/clickhouse/live/named-collections.d.ts.map +1 -0
  12. package/dist/clickhouse/live/named-collections.js +53 -0
  13. package/dist/clickhouse/live/undeclarable.d.ts +15 -3
  14. package/dist/clickhouse/live/undeclarable.d.ts.map +1 -1
  15. package/dist/clickhouse/live/undeclarable.js +18 -4
  16. package/dist/clickhouse/named-collections.d.ts +83 -0
  17. package/dist/clickhouse/named-collections.d.ts.map +1 -0
  18. package/dist/clickhouse/named-collections.js +203 -0
  19. package/dist/clickhouse/parser.d.ts.map +1 -1
  20. package/dist/clickhouse/parser.js +1 -1
  21. package/dist/clickhouse/plan/commands.d.ts +2 -1
  22. package/dist/clickhouse/plan/commands.d.ts.map +1 -1
  23. package/dist/clickhouse/plan/commands.js +17 -2
  24. package/dist/clickhouse/plan/diff.d.ts +15 -0
  25. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  26. package/dist/clickhouse/plan/diff.js +43 -1
  27. package/dist/clickhouse/plan/disruption.d.ts.map +1 -1
  28. package/dist/clickhouse/plan/disruption.js +5 -0
  29. package/dist/clickhouse/plan/rebuild-handoff.d.ts.map +1 -1
  30. package/dist/clickhouse/plan/rebuild-handoff.js +4 -0
  31. package/dist/clickhouse/plan/report.d.ts.map +1 -1
  32. package/dist/clickhouse/plan/report.js +6 -1
  33. package/dist/clickhouse/plan/rules.d.ts +5 -1
  34. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  35. package/dist/clickhouse/plan/rules.js +7 -3
  36. package/dist/clickhouse/plan/statements.d.ts +29 -0
  37. package/dist/clickhouse/plan/statements.d.ts.map +1 -0
  38. package/dist/clickhouse/plan/statements.js +334 -0
  39. package/dist/clickhouse/plan/stream.d.ts +40 -0
  40. package/dist/clickhouse/plan/stream.d.ts.map +1 -0
  41. package/dist/clickhouse/plan/stream.js +107 -0
  42. package/dist/clickhouse/rebuild/observe.d.ts +3 -1
  43. package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
  44. package/dist/clickhouse/rebuild/observe.js +14 -1
  45. package/dist/clickhouse/review-catalog.d.ts +213 -0
  46. package/dist/clickhouse/review-catalog.d.ts.map +1 -0
  47. package/dist/clickhouse/review-catalog.js +330 -0
  48. package/dist/codegen/classifier-page.d.ts.map +1 -1
  49. package/dist/codegen/classifier-page.js +44 -4
  50. package/dist/codegen/clickhouse-module.d.ts.map +1 -1
  51. package/dist/codegen/clickhouse-module.js +3 -0
  52. package/dist/codegen/coverage-page.d.ts.map +1 -1
  53. package/dist/codegen/coverage-page.js +5 -3
  54. package/dist/codegen/docs.js +1 -1
  55. package/dist/codegen/postgres-classifier-page.js +2 -2
  56. package/dist/codegen/registry.d.ts.map +1 -1
  57. package/dist/codegen/registry.js +10 -0
  58. package/dist/composites/catalog.d.ts.map +1 -1
  59. package/dist/composites/catalog.js +100 -0
  60. package/dist/composites/clickhouse.d.ts +2 -0
  61. package/dist/composites/clickhouse.d.ts.map +1 -1
  62. package/dist/composites/clickhouse.js +2 -0
  63. package/dist/composites/otel-tables.d.ts +100 -0
  64. package/dist/composites/otel-tables.d.ts.map +1 -0
  65. package/dist/composites/otel-tables.js +282 -0
  66. package/dist/composites/stream-ingest.d.ts +63 -0
  67. package/dist/composites/stream-ingest.d.ts.map +1 -0
  68. package/dist/composites/stream-ingest.js +39 -0
  69. package/dist/core/statement-class.d.ts +55 -0
  70. package/dist/core/statement-class.d.ts.map +1 -0
  71. package/dist/core/statement-class.js +43 -0
  72. package/dist/coverage/clickhouse.json +1 -1
  73. package/dist/coverage/postgres.json +12 -12
  74. package/dist/coverage.d.ts +16 -6
  75. package/dist/coverage.d.ts.map +1 -1
  76. package/dist/coverage.js +10 -5
  77. package/dist/drizzle-command.d.ts +17 -0
  78. package/dist/drizzle-command.d.ts.map +1 -0
  79. package/dist/drizzle-command.js +91 -0
  80. package/dist/drizzle.d.ts +70 -0
  81. package/dist/drizzle.d.ts.map +1 -0
  82. package/dist/drizzle.js +689 -0
  83. package/dist/files/clickhouse.d.ts.map +1 -1
  84. package/dist/files/clickhouse.js +8 -1
  85. package/dist/files/common.d.ts +13 -0
  86. package/dist/files/common.d.ts.map +1 -1
  87. package/dist/files/common.js +5 -0
  88. package/dist/files/postgres.d.ts.map +1 -1
  89. package/dist/files/postgres.js +35 -3
  90. package/dist/generated/clickhouse.d.ts +2 -0
  91. package/dist/generated/clickhouse.d.ts.map +1 -1
  92. package/dist/generated/clickhouse.js +2 -0
  93. package/dist/import-parser.d.ts +21 -1
  94. package/dist/import-parser.d.ts.map +1 -1
  95. package/dist/import-parser.js +79 -5
  96. package/dist/index.d.ts +1 -1
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +1 -1
  99. package/dist/integrity.json +7 -6
  100. package/dist/lint/post-synth/audit-entries.d.ts.map +1 -1
  101. package/dist/lint/post-synth/audit-entries.js +1 -0
  102. package/dist/lint/post-synth/index.d.ts.map +1 -1
  103. package/dist/lint/post-synth/index.js +2 -0
  104. package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
  105. package/dist/lint/post-synth/postgres-names.js +3 -0
  106. package/dist/lint/post-synth/sqlch128.d.ts +18 -0
  107. package/dist/lint/post-synth/sqlch128.d.ts.map +1 -0
  108. package/dist/lint/post-synth/sqlch128.js +222 -0
  109. package/dist/lint/rules/postgres-templates.js +2 -2
  110. package/dist/lsp/postgres-template.d.ts.map +1 -1
  111. package/dist/lsp/postgres-template.js +21 -0
  112. package/dist/manifest.json +1 -1
  113. package/dist/mcp/postgres.js +1 -1
  114. package/dist/meta.json +50 -0
  115. package/dist/migrate/from-postgres/detect.d.ts +4 -0
  116. package/dist/migrate/from-postgres/detect.d.ts.map +1 -0
  117. package/dist/migrate/from-postgres/detect.js +7 -0
  118. package/dist/migrate/from-postgres/emit.d.ts +26 -0
  119. package/dist/migrate/from-postgres/emit.d.ts.map +1 -0
  120. package/dist/migrate/from-postgres/emit.js +86 -0
  121. package/dist/migrate/from-postgres/index.d.ts +51 -0
  122. package/dist/migrate/from-postgres/index.d.ts.map +1 -0
  123. package/dist/migrate/from-postgres/index.js +115 -0
  124. package/dist/migrate/from-postgres/translate.d.ts +73 -0
  125. package/dist/migrate/from-postgres/translate.d.ts.map +1 -0
  126. package/dist/migrate/from-postgres/translate.js +261 -0
  127. package/dist/migrate/from-postgres/types.d.ts +51 -0
  128. package/dist/migrate/from-postgres/types.d.ts.map +1 -0
  129. package/dist/migrate/from-postgres/types.js +211 -0
  130. package/dist/migration-classify.d.ts +23 -0
  131. package/dist/migration-classify.d.ts.map +1 -0
  132. package/dist/migration-classify.js +44 -0
  133. package/dist/migration-replay.d.ts +73 -0
  134. package/dist/migration-replay.d.ts.map +1 -0
  135. package/dist/migration-replay.js +294 -0
  136. package/dist/okf/index.md +11 -0
  137. package/dist/okf/rules/SQLCH128.md +11 -0
  138. package/dist/okf/types/PostgresForeignDataWrapper.md +9 -0
  139. package/dist/okf/types/PostgresForeignServer.md +9 -0
  140. package/dist/okf/types/PostgresForeignTable.md +9 -0
  141. package/dist/okf/types/PostgresOperator.md +9 -0
  142. package/dist/okf/types/PostgresOperatorClass.md +9 -0
  143. package/dist/okf/types/PostgresOperatorFamily.md +9 -0
  144. package/dist/okf/types/PostgresPublication.md +9 -0
  145. package/dist/okf/types/PostgresStatistics.md +9 -0
  146. package/dist/okf/types/PostgresTextSearchConfiguration.md +9 -0
  147. package/dist/okf/types/PostgresTextSearchDictionary.md +9 -0
  148. package/dist/op/activities/postgres-migration.d.ts.map +1 -1
  149. package/dist/op/activities/postgres-migration.js +4 -0
  150. package/dist/op/activity-contracts.d.ts +20 -0
  151. package/dist/op/activity-contracts.d.ts.map +1 -1
  152. package/dist/op/activity-contracts.js +2 -0
  153. package/dist/plugin.d.ts.map +1 -1
  154. package/dist/plugin.js +49 -2
  155. package/dist/postgres/access/acl.js +1 -1
  156. package/dist/postgres/access/import.js +1 -1
  157. package/dist/postgres/apply/apply.js +13 -1
  158. package/dist/postgres/apply/statements.d.ts.map +1 -1
  159. package/dist/postgres/apply/statements.js +243 -4
  160. package/dist/postgres/constraint-names.d.ts +30 -0
  161. package/dist/postgres/constraint-names.d.ts.map +1 -0
  162. package/dist/postgres/constraint-names.js +72 -0
  163. package/dist/postgres/entities.d.ts +229 -2
  164. package/dist/postgres/entities.d.ts.map +1 -1
  165. package/dist/postgres/entities.js +326 -5
  166. package/dist/postgres/entity-types.d.ts +10 -0
  167. package/dist/postgres/entity-types.d.ts.map +1 -1
  168. package/dist/postgres/entity-types.js +10 -0
  169. package/dist/postgres/import/generator.d.ts.map +1 -1
  170. package/dist/postgres/import/generator.js +65 -2
  171. package/dist/postgres/import/live-export.d.ts.map +1 -1
  172. package/dist/postgres/import/live-export.js +2 -0
  173. package/dist/postgres/live/catalog-kinds.d.ts +20 -0
  174. package/dist/postgres/live/catalog-kinds.d.ts.map +1 -1
  175. package/dist/postgres/live/catalog-kinds.js +452 -0
  176. package/dist/postgres/live/catalog.d.ts +2 -0
  177. package/dist/postgres/live/catalog.d.ts.map +1 -1
  178. package/dist/postgres/live/catalog.js +3 -1
  179. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  180. package/dist/postgres/live/describe-resources.js +12 -2
  181. package/dist/postgres/migrate/backfill.d.ts +2 -0
  182. package/dist/postgres/migrate/backfill.d.ts.map +1 -1
  183. package/dist/postgres/migrate/backfill.js +7 -1
  184. package/dist/postgres/migrate/carry.d.ts +45 -1
  185. package/dist/postgres/migrate/carry.d.ts.map +1 -1
  186. package/dist/postgres/migrate/carry.js +72 -8
  187. package/dist/postgres/migrate/observe.d.ts +28 -4
  188. package/dist/postgres/migrate/observe.d.ts.map +1 -1
  189. package/dist/postgres/migrate/observe.js +45 -13
  190. package/dist/postgres/migrate/op.d.ts +26 -1
  191. package/dist/postgres/migrate/op.d.ts.map +1 -1
  192. package/dist/postgres/migrate/op.js +3 -1
  193. package/dist/postgres/migrate/replication.d.ts +42 -0
  194. package/dist/postgres/migrate/replication.d.ts.map +1 -1
  195. package/dist/postgres/migrate/replication.js +53 -0
  196. package/dist/postgres/migrate/steps.d.ts +35 -2
  197. package/dist/postgres/migrate/steps.d.ts.map +1 -1
  198. package/dist/postgres/migrate/steps.js +158 -12
  199. package/dist/postgres/parser.d.ts +144 -1
  200. package/dist/postgres/parser.d.ts.map +1 -1
  201. package/dist/postgres/parser.js +370 -4
  202. package/dist/postgres/plan/deep.d.ts.map +1 -1
  203. package/dist/postgres/plan/deep.js +43 -4
  204. package/dist/postgres/plan/diff.d.ts.map +1 -1
  205. package/dist/postgres/plan/diff.js +154 -1
  206. package/dist/postgres/plan/normalize.d.ts +28 -2
  207. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  208. package/dist/postgres/plan/normalize.js +260 -1
  209. package/dist/postgres/plan/rules.d.ts +4 -0
  210. package/dist/postgres/plan/rules.d.ts.map +1 -1
  211. package/dist/postgres/plan/rules.js +6 -2
  212. package/dist/postgres/plan/schema.js +1 -1
  213. package/dist/postgres/plan/statements.d.ts +26 -0
  214. package/dist/postgres/plan/statements.d.ts.map +1 -0
  215. package/dist/postgres/plan/statements.js +364 -0
  216. package/dist/postgres/secret-options.d.ts +10 -0
  217. package/dist/postgres/secret-options.d.ts.map +1 -0
  218. package/dist/postgres/secret-options.js +9 -0
  219. package/dist/postgres/testing/roundtrip-schema.d.ts +4 -2
  220. package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
  221. package/dist/postgres/testing/roundtrip-schema.js +24 -1
  222. package/dist/postgres.d.ts +1 -1
  223. package/dist/postgres.d.ts.map +1 -1
  224. package/dist/postgres.js +1 -1
  225. package/dist/rules/audit-entries.ts +1 -0
  226. package/dist/rules/postgres-names.ts +2 -0
  227. package/dist/rules/postgres-templates.ts +2 -2
  228. package/dist/rules/sqlch128.ts +230 -0
  229. package/dist/serializer.d.ts +5 -0
  230. package/dist/serializer.d.ts.map +1 -1
  231. package/dist/serializer.js +13 -2
  232. package/dist/sql-types-command.d.ts +13 -0
  233. package/dist/sql-types-command.d.ts.map +1 -1
  234. package/dist/sql-types-command.js +31 -16
  235. package/dist/sql-types.d.ts +22 -0
  236. package/dist/sql-types.d.ts.map +1 -1
  237. package/dist/sql-types.js +10 -6
  238. package/dist/sqlserver/lexer.d.ts +37 -0
  239. package/dist/sqlserver/lexer.d.ts.map +1 -0
  240. package/dist/sqlserver/lexer.js +254 -0
  241. package/dist/sqlserver/parse.d.ts +185 -0
  242. package/dist/sqlserver/parse.d.ts.map +1 -0
  243. package/dist/sqlserver/parse.js +767 -0
  244. package/dist/sqlserver/translate.d.ts +34 -0
  245. package/dist/sqlserver/translate.d.ts.map +1 -0
  246. package/dist/sqlserver/translate.js +495 -0
  247. package/dist/sqlserver/types.d.ts +44 -0
  248. package/dist/sqlserver/types.d.ts.map +1 -0
  249. package/dist/sqlserver/types.js +133 -0
  250. package/dist/sqlserver/validate.d.ts +31 -0
  251. package/dist/sqlserver/validate.d.ts.map +1 -0
  252. package/dist/sqlserver/validate.js +67 -0
  253. package/dist/validate.d.ts.map +1 -1
  254. package/dist/validate.js +10 -0
  255. package/package.json +2 -2
  256. package/src/change-set.ts +2 -1
  257. package/src/clickhouse/apply/statements.ts +25 -0
  258. package/src/clickhouse/import/live-export.ts +20 -5
  259. package/src/clickhouse/live/named-collections.e2e.test.ts +88 -0
  260. package/src/clickhouse/live/named-collections.ts +61 -0
  261. package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -3
  262. package/src/clickhouse/live/undeclarable.ts +20 -4
  263. package/src/clickhouse/named-collections.test.ts +187 -0
  264. package/src/clickhouse/named-collections.ts +224 -0
  265. package/src/clickhouse/parser.ts +2 -1
  266. package/src/clickhouse/plan/commands.ts +17 -2
  267. package/src/clickhouse/plan/diff.ts +57 -1
  268. package/src/clickhouse/plan/disruption.ts +5 -0
  269. package/src/clickhouse/plan/rebuild-handoff.ts +3 -0
  270. package/src/clickhouse/plan/report.ts +6 -1
  271. package/src/clickhouse/plan/rules.ts +14 -4
  272. package/src/clickhouse/plan/statements.test.ts +61 -0
  273. package/src/clickhouse/plan/statements.ts +306 -0
  274. package/src/clickhouse/plan/stream-s3queue.e2e.test.ts +148 -0
  275. package/src/clickhouse/plan/stream.test.ts +237 -0
  276. package/src/clickhouse/plan/stream.ts +111 -0
  277. package/src/clickhouse/rebuild/observe.ts +15 -1
  278. package/src/clickhouse/review-catalog.test.ts +181 -0
  279. package/src/clickhouse/review-catalog.ts +489 -0
  280. package/src/codegen/classifier-page.ts +44 -4
  281. package/src/codegen/clickhouse-module.ts +3 -0
  282. package/src/codegen/coverage-page.ts +5 -3
  283. package/src/codegen/docs.ts +1 -1
  284. package/src/codegen/postgres-classifier-page.ts +2 -2
  285. package/src/codegen/registry.ts +10 -0
  286. package/src/composites/catalog.test.ts +6 -1
  287. package/src/composites/catalog.ts +100 -0
  288. package/src/composites/clickhouse.ts +2 -0
  289. package/src/composites/composites.test.ts +1 -1
  290. package/src/composites/otel-tables.test.ts +118 -0
  291. package/src/composites/otel-tables.ts +339 -0
  292. package/src/composites/stream-ingest.test.ts +114 -0
  293. package/src/composites/stream-ingest.ts +82 -0
  294. package/src/core/statement-class.ts +88 -0
  295. package/src/core/undeclarable.test.ts +6 -5
  296. package/src/coverage/clickhouse.json +1 -1
  297. package/src/coverage/postgres.json +12 -12
  298. package/src/coverage.test.ts +27 -2
  299. package/src/coverage.ts +18 -7
  300. package/src/drizzle-command.ts +94 -0
  301. package/src/drizzle-kit.e2e.test.ts +89 -0
  302. package/src/drizzle-types.e2e.test.ts +67 -0
  303. package/src/drizzle.test.ts +358 -0
  304. package/src/drizzle.ts +742 -0
  305. package/src/files/clickhouse.ts +8 -1
  306. package/src/files/common.ts +14 -1
  307. package/src/files/postgres.ts +34 -3
  308. package/src/generated/clickhouse.ts +3 -0
  309. package/src/generated/lexicon-sql.json +50 -0
  310. package/src/import-parser.sqlserver.test.ts +92 -0
  311. package/src/import-parser.ts +79 -5
  312. package/src/index.ts +1 -1
  313. package/src/lint/post-synth/audit-entries.ts +1 -0
  314. package/src/lint/post-synth/index.ts +2 -0
  315. package/src/lint/post-synth/postgres-checks.test.ts +8 -1
  316. package/src/lint/post-synth/postgres-names.ts +2 -0
  317. package/src/lint/post-synth/sqlch128.test.ts +107 -0
  318. package/src/lint/post-synth/sqlch128.ts +230 -0
  319. package/src/lint/rules/postgres-templates.ts +2 -2
  320. package/src/lsp/postgres-template.ts +20 -0
  321. package/src/mcp/postgres.ts +1 -1
  322. package/src/migrate/from-postgres/cli.test.ts +165 -0
  323. package/src/migrate/from-postgres/detect.ts +7 -0
  324. package/src/migrate/from-postgres/emit.ts +96 -0
  325. package/src/migrate/from-postgres/example.test.ts +205 -0
  326. package/src/migrate/from-postgres/index.ts +122 -0
  327. package/src/migrate/from-postgres/translate.test.ts +242 -0
  328. package/src/migrate/from-postgres/translate.ts +359 -0
  329. package/src/migrate/from-postgres/types.test.ts +116 -0
  330. package/src/migrate/from-postgres/types.ts +236 -0
  331. package/src/migrate/from-postgres/validate.e2e.test.ts +44 -0
  332. package/src/migrate/from-postgres/workspace.e2e.test.ts +114 -0
  333. package/src/migration-classify.test.ts +150 -0
  334. package/src/migration-classify.ts +45 -0
  335. package/src/migration-replay.e2e.test.ts +155 -0
  336. package/src/migration-replay.test.ts +173 -0
  337. package/src/migration-replay.ts +289 -0
  338. package/src/op/__snapshots__/watch-pipeline.test.ts.snap +2 -2
  339. package/src/op/activities/postgres-migration.ts +4 -0
  340. package/src/op/activity-contracts.ts +2 -0
  341. package/src/op/watch-pipeline.test.ts +8 -1
  342. package/src/plugin.ts +51 -2
  343. package/src/postgres/access/acl.ts +1 -1
  344. package/src/postgres/access/import.ts +1 -1
  345. package/src/postgres/apply/apply.ts +14 -1
  346. package/src/postgres/apply/statements.ts +225 -4
  347. package/src/postgres/constraint-names.ts +71 -0
  348. package/src/postgres/database-kinds.test.ts +865 -0
  349. package/src/postgres/entities.ts +541 -5
  350. package/src/postgres/entity-types.ts +10 -0
  351. package/src/postgres/import/generator.ts +54 -2
  352. package/src/postgres/import/live-export.ts +1 -0
  353. package/src/postgres/live/catalog-kinds.ts +494 -0
  354. package/src/postgres/live/catalog.ts +6 -1
  355. package/src/postgres/live/database-kinds.e2e.test.ts +164 -0
  356. package/src/postgres/live/describe-resources.ts +13 -2
  357. package/src/postgres/live/undeclarable.e2e.test.ts +13 -6
  358. package/src/postgres/migrate/backfill.ts +13 -1
  359. package/src/postgres/migrate/carry.ts +103 -8
  360. package/src/postgres/migrate/followups.e2e.test.ts +218 -0
  361. package/src/postgres/migrate/followups.test.ts +199 -0
  362. package/src/postgres/migrate/kinds.e2e.test.ts +5 -5
  363. package/src/postgres/migrate/observe.ts +56 -15
  364. package/src/postgres/migrate/op.ts +28 -1
  365. package/src/postgres/migrate/partition.e2e.test.ts +129 -0
  366. package/src/postgres/migrate/partition.test.ts +206 -0
  367. package/src/postgres/migrate/replication.ts +66 -0
  368. package/src/postgres/migrate/steps.ts +157 -12
  369. package/src/postgres/migrate/subscriber.e2e.test.ts +141 -0
  370. package/src/postgres/migrate/subscriber.test.ts +59 -0
  371. package/src/postgres/parser.ts +455 -4
  372. package/src/postgres/plan/deep.ts +42 -4
  373. package/src/postgres/plan/diff.ts +137 -1
  374. package/src/postgres/plan/normalize.ts +277 -1
  375. package/src/postgres/plan/rules.ts +32 -4
  376. package/src/postgres/plan/schema.ts +1 -1
  377. package/src/postgres/plan/statements.test.ts +78 -0
  378. package/src/postgres/plan/statements.ts +299 -0
  379. package/src/postgres/secret-options.ts +10 -0
  380. package/src/postgres/testing/roundtrip-schema.ts +24 -1
  381. package/src/postgres.ts +33 -0
  382. package/src/serializer.ts +13 -2
  383. package/src/skills/chant-sql-postgres-migration.md +4 -3
  384. package/src/skills/chant-sql-postgres.md +11 -1
  385. package/src/skills/chant-sql.md +2 -1
  386. package/src/sql-files.test.ts +33 -0
  387. package/src/sql-types-command.ts +38 -16
  388. package/src/sql-types.ts +11 -6
  389. package/src/sqlserver/fixture.test.ts +186 -0
  390. package/src/sqlserver/import-cli.test.ts +61 -0
  391. package/src/sqlserver/lexer.ts +260 -0
  392. package/src/sqlserver/parse.ts +865 -0
  393. package/src/sqlserver/translate.test.ts +396 -0
  394. package/src/sqlserver/translate.ts +558 -0
  395. package/src/sqlserver/types.test.ts +102 -0
  396. package/src/sqlserver/types.ts +154 -0
  397. package/src/sqlserver/validate.e2e.test.ts +32 -0
  398. package/src/sqlserver/validate.test.ts +49 -0
  399. package/src/sqlserver/validate.ts +76 -0
  400. package/src/validate.ts +10 -0
  401. package/src/workspace-kinds.test.ts +11 -0
  402. package/workspace-kinds.json +100 -2
@@ -25,7 +25,7 @@ import { col, migrationNames, MIGRATION_TRAILER_KEY, type MigrationNames } from
25
25
  import { MigrationRefusal, observeMigration, type MigrationObservation } from "./observe";
26
26
  import { postgresReceiptStore, receiptAddress } from "./receipts";
27
27
  import type { ReplicationLagBound } from "./replication";
28
- import { addCarriedConstraint, carriedNotReady, carriedSwitchStatements, requiresNotNull, SERIAL_TYPES, storedType, type CarriedObject } from "./carry";
28
+ import { addCarriedConstraint, carriedNotReady, carriedSwitchStatements, partitionIndexName, partitionIndexStatement, partitionTree, requiresNotNull, SERIAL_TYPES, storedType, type CarriedObject, type CarriedState } from "./carry";
29
29
 
30
30
  /** What every step is given. */
31
31
  export interface MigrationRun {
@@ -39,6 +39,10 @@ export interface MigrationRun {
39
39
  column: string;
40
40
  /** The type change's expression over the old row's columns. */
41
41
  using?: string;
42
+ /** A rename with a type change: the expression that gives the old column's value from the new column's. */
43
+ reverse?: string;
44
+ /** A rename of a column with a default, serial or identity: the declared default lands on the new column at the contract. */
45
+ defaultAtContract?: boolean;
42
46
  /** This project's ownership marker, stamped on every working object. */
43
47
  marker?: OwnershipMarker;
44
48
  /** The major the build targets. */
@@ -70,6 +74,8 @@ export const observe = (run: MigrationRun): Promise<MigrationObservation> =>
70
74
  ...(run.objects ? { objects: run.objects } : {}),
71
75
  ...(run.marker ? { marker: run.marker } : {}),
72
76
  ...(run.using !== undefined ? { using: run.using } : {}),
77
+ ...(run.reverse !== undefined ? { reverse: run.reverse } : {}),
78
+ ...(run.defaultAtContract ? { defaultAtContract: true } : {}),
73
79
  ...(run.major !== undefined ? { major: run.major } : {}),
74
80
  });
75
81
 
@@ -174,7 +180,7 @@ export interface ExpandResult {
174
180
  }
175
181
 
176
182
  /** The new column's type: the declared one, with its collation when it declares one. */
177
- const newColumnType = (o: MigrationObservation): string => `${o.names.change === "rename" ? o.source!.type : storedType(o.column.type)}${o.column.collate ? ` COLLATE ${o.column.collate}` : ""}`;
183
+ const newColumnType = (o: MigrationObservation): string => `${o.names.change === "rename" && o.reverse === undefined ? o.source!.type : storedType(o.column.type)}${o.column.collate ? ` COLLATE ${o.column.collate}` : ""}`;
178
184
 
179
185
  /**
180
186
  * Expand: add the new column, nullable and with no default, which changes
@@ -211,10 +217,27 @@ export async function expand(run: MigrationRun): Promise<ExpandResult> {
211
217
  * An insert takes the new column's value when it is given and the old one's
212
218
  * otherwise; an update takes whichever of the two it changed.
213
219
  */
214
- export function dualWriteBody(o: Pick<MigrationObservation, "names" | "expression">): string {
220
+ export function dualWriteBody(o: Pick<MigrationObservation, "names" | "expression"> & { reverse?: string }): string {
215
221
  const n = o.names;
216
222
  const nw = `NEW.${col(n.newColumn)}`;
217
223
  const src = `NEW.${col(n.source)}`;
224
+ if (n.change === "rename" && o.reverse !== undefined) {
225
+ // A rename with a type change: a writer of the old name has `expression` applied, a writer of the new name `reverse`, each over the row as it is.
226
+ const fwd = `(SELECT ${o.expression} FROM (SELECT NEW.*) AS ${col(n.table)})`;
227
+ const back = `(SELECT ${o.reverse} FROM (SELECT NEW.*) AS ${col(n.table)})`;
228
+ return [
229
+ `BEGIN`,
230
+ ` IF TG_OP = 'INSERT' THEN`,
231
+ ` IF ${nw} IS NULL THEN ${nw} := ${fwd}; ELSE ${src} := ${back}; END IF;`,
232
+ ` ELSIF ${nw} IS DISTINCT FROM OLD.${col(n.newColumn)} THEN`,
233
+ ` ${src} := ${back};`,
234
+ ` ELSE`,
235
+ ` ${nw} := ${fwd};`,
236
+ ` END IF;`,
237
+ ` RETURN NEW;`,
238
+ `END`,
239
+ ].join("\n");
240
+ }
218
241
  if (n.change === "type") {
219
242
  return [`BEGIN`, ` ${nw} := (SELECT ${o.expression} FROM (SELECT NEW.*) AS ${col(n.table)});`, ` RETURN NEW;`, `END`].join("\n");
220
243
  }
@@ -233,7 +256,7 @@ export function dualWriteBody(o: Pick<MigrationObservation, "names" | "expressio
233
256
  }
234
257
 
235
258
  /** The statements that start the dual write: the function, the trigger, their comments. */
236
- export function dualWriteStatements(o: Pick<MigrationObservation, "names" | "expression">, marker: OwnershipMarker | undefined, defaultSchema: string): string[] {
259
+ export function dualWriteStatements(o: Pick<MigrationObservation, "names" | "expression"> & { reverse?: string }, marker: OwnershipMarker | undefined, defaultSchema: string): string[] {
237
260
  const n = o.names;
238
261
  return [
239
262
  `CREATE OR REPLACE FUNCTION ${n.qualifiedFn}() RETURNS trigger LANGUAGE plpgsql SET search_path = ${quoteIdent(defaultSchema)} AS $chant$\n${dualWriteBody(o)}\n$chant$`,
@@ -304,6 +327,11 @@ export async function carryOver(run: MigrationRun): Promise<CarryResult> {
304
327
  result.carried.push(`${c.kind} ${c.tableName}.${c.name} -> ${c.target}`);
305
328
  const state = o.carriedStates.get(c.working);
306
329
  const comment = workingComment(n, run.marker, "carry", `${c.kind} ${c.name} on the new column`, { of: c.name });
330
+ if (c.kind === "index" && c.partitioned) {
331
+ if (await carryPartitionedIndex(run, n, c, comment, state)) result.built++;
332
+ else result.ready++;
333
+ continue;
334
+ }
307
335
  if (c.kind === "index" || c.kind === "key") {
308
336
  const ident = qualifiedIndex(c, c.working);
309
337
  if (state?.ready) {
@@ -340,6 +368,85 @@ export async function carryOver(run: MigrationRun): Promise<CarryResult> {
340
368
  return result;
341
369
  }
342
370
 
371
+ /**
372
+ * Carry over one index of a partitioned table (#3333). A partitioned index
373
+ * cannot be built `CONCURRENTLY`, so it is made the way the Postgres docs
374
+ * give for adding one without blocking writes: the partitioned index
375
+ * `ON ONLY` the table (nothing is built, and it is INVALID), each leaf
376
+ * partition's index built `CONCURRENTLY` on its own, then
377
+ * `ALTER INDEX ... ATTACH PARTITION` for each, deepest first. The partitioned
378
+ * index becomes valid when its last partition is attached. A partition
379
+ * attached to the table meanwhile gets its index from Postgres, and one that
380
+ * already has an index attached to this one is left alone, so a run after a
381
+ * crash goes on from where the last stopped. Returns whether anything was
382
+ * made.
383
+ */
384
+ export async function carryPartitionedIndex(run: MigrationRun, n: MigrationNames, c: CarriedObject, comment: string, state: CarriedState | undefined): Promise<boolean> {
385
+ if (state?.ready && state.marked) return false;
386
+ const root = qualifiedIndex(c, c.working);
387
+ const one = async <T>(sql: string, params: unknown[]): Promise<T | undefined> => (await run.client.query<T>(sql, params))[0];
388
+ const oidOf = async (ident: string): Promise<string | undefined> => (await one<{ oid: string | null }>("SELECT pg_catalog.to_regclass($1)::oid::text AS oid", [ident]))?.oid ?? undefined;
389
+ /** The index on `partitionOid` that is attached to `parentIndexOid`, whoever made it. */
390
+ const attachedIndex = async (parentIndexOid: string, partitionOid: string): Promise<{ oid: string; ident: string } | undefined> => {
391
+ const row = await one<{ oid: string; schema: string; name: string }>(
392
+ `SELECT x.oid::text AS oid, s.nspname AS schema, x.relname AS name FROM pg_catalog.pg_inherits h JOIN pg_catalog.pg_index i ON i.indexrelid = h.inhrelid
393
+ JOIN pg_catalog.pg_class x ON x.oid = i.indexrelid JOIN pg_catalog.pg_namespace s ON s.oid = x.relnamespace WHERE h.inhparent = $1::oid AND i.indrelid = $2::oid`,
394
+ [parentIndexOid, partitionOid],
395
+ );
396
+ return row ? { oid: String(row.oid), ident: `${quoteIdent(row.schema)}.${quoteIdent(row.name)}` } : undefined;
397
+ };
398
+ if (!state?.present) {
399
+ await inTransaction(run, async (exec) => {
400
+ await exec(c.definition);
401
+ await exec(`COMMENT ON INDEX ${root} IS ${pgString(comment)}`);
402
+ });
403
+ } else if (!state.marked) {
404
+ await inTransaction(run, (exec) => exec(`COMMENT ON INDEX ${root} IS ${pgString(comment)}`));
405
+ }
406
+ const tree = (await partitionTree(run.client, c.tableOid)).filter((p) => p.oid !== c.tableOid);
407
+ const byOid = new Map(tree.map((p) => [p.oid, p]));
408
+ const ours = (p: { schema: string; name: string }) => `${quoteIdent(p.schema)}.${quoteIdent(partitionIndexName(p, c.working))}`;
409
+ const rootIndex = { oid: (await oidOf(root))!, ident: root };
410
+ /** The index a partition's index attaches to: the partitioned index above it, as it is now. */
411
+ const above = async (p: { parent?: string }): Promise<{ oid: string; ident: string }> => {
412
+ if (p.parent === c.tableOid || p.parent === undefined) return rootIndex;
413
+ const parent = byOid.get(p.parent)!;
414
+ const up = await above(parent);
415
+ return (await attachedIndex(up.oid, parent.oid)) ?? { oid: (await oidOf(ours(parent)))!, ident: ours(parent) };
416
+ };
417
+ // Top down: each partition without an index attached to its parent's gets a standalone one.
418
+ const pending: Array<{ p: (typeof tree)[number]; parent: { oid: string; ident: string } }> = [];
419
+ for (const p of tree) {
420
+ const parent = await above(p);
421
+ if (await attachedIndex(parent.oid, p.oid)) continue;
422
+ const ident = ours(p);
423
+ const cur = await one<{ valid: boolean; comment: string | null }>(
424
+ "SELECT i.indisvalid AS valid, pg_catalog.obj_description(i.indexrelid, 'pg_class') AS comment FROM pg_catalog.pg_index i WHERE i.indexrelid = pg_catalog.to_regclass($1)",
425
+ [ident],
426
+ );
427
+ if (cur && !cur.valid) await outsideTransaction(run, `DROP INDEX CONCURRENTLY IF EXISTS ${ident}`);
428
+ if (!cur || !cur.valid) {
429
+ const statement = partitionIndexStatement(c, p, partitionIndexName(p, c.working));
430
+ const dropInvalid = async () => {
431
+ const row = await one<{ invalid: boolean }>("SELECT NOT i.indisvalid AS invalid FROM pg_catalog.pg_index i WHERE i.indexrelid = pg_catalog.to_regclass($1)", [ident]);
432
+ if (row?.invalid) await run.client.query(`DROP INDEX CONCURRENTLY IF EXISTS ${ident}`);
433
+ };
434
+ if (p.leaf) await outsideTransaction(run, statement, { scan: true, cleanup: dropInvalid });
435
+ else await inTransaction(run, (exec) => exec(statement));
436
+ }
437
+ if (!cur || !cur.valid || cur.comment == null) {
438
+ const partitionComment = workingComment(n, run.marker, "carry", `${c.kind} ${c.name} on the new column, for partition ${p.name}`, { of: c.name });
439
+ await inTransaction(run, (exec) => exec(`COMMENT ON INDEX ${ident} IS ${pgString(partitionComment)}`));
440
+ }
441
+ pending.push({ p, parent });
442
+ }
443
+ // Bottom up: a partition's index is valid once its own partitions are attached.
444
+ for (const { p, parent } of [...pending].sort((a, b) => b.p.level - a.p.level)) {
445
+ await inTransaction(run, (exec) => exec(`ALTER INDEX ${parent.ident} ATTACH PARTITION ${ours(p)}`));
446
+ }
447
+ return true;
448
+ }
449
+
343
450
  // ── switch ─────────────────────────────────────────────────────────────
344
451
 
345
452
  export interface SwitchResult {
@@ -373,13 +480,17 @@ export interface SequenceState {
373
480
  * `serial` column's sequence is handed over with `OWNED BY`, takes the
374
481
  * declared integer type, and becomes the new column's default.
375
482
  */
376
- function sequenceStatements(o: MigrationObservation, state: SequenceState | undefined): { before: string[]; after: string[] } {
483
+ function sequenceStatements(o: MigrationObservation, state: SequenceState | undefined, at: "switch" | "contract" = "switch"): { before: string[]; after: string[] } {
377
484
  const seq = o.sequence;
378
485
  const n = o.names;
379
486
  const t = n.qualifiedTable;
380
- if (!seq || n.change !== "type") return { before: [], after: [] };
487
+ if (!seq || n.change !== (at === "switch" ? "type" : "rename")) return { before: [], after: [] };
488
+ if (at === "contract" && seq.kind === "serial") {
489
+ // The sequence goes to the new column before the old one is dropped, which would drop it; the default is set after.
490
+ return { before: [`ALTER SEQUENCE ${seq.ident} OWNED BY ${t}.${col(n.column)}`], after: [] };
491
+ }
381
492
  if (seq.kind === "identity") {
382
- if (!state) throw new Error(`${n.key}: the switch moves the identity of ${n.source} and needs its sequence's state, read under the switch's lock`);
493
+ if (!state) throw new Error(`${n.key}: the ${at} moves the identity of ${n.source} and needs its sequence's state, read under the switch's lock`);
383
494
  const g = o.column.generated;
384
495
  const options = [`SEQUENCE NAME ${seq.ident}`, ...(g?.options ? [g.options.trim().replace(/^\(|\)$/g, "")] : [])].join(" ");
385
496
  return {
@@ -540,6 +651,31 @@ export const identityOf = (marker: OwnershipMarker | undefined): { stack?: strin
540
651
  * catalog change under ACCESS EXCLUSIVE). The migration's receipts go with
541
652
  * them. Before the date, say until when and drop nothing.
542
653
  */
654
+ /**
655
+ * The contract's statements. Always: the trigger and its function, then the
656
+ * old column. A rename of a column with a default, serial or identity
657
+ * (`defaultAtContract`) also gives the declared default to the new column
658
+ * here, in the same transaction, so no writer sees both columns defaulted:
659
+ * a serial's sequence is handed over before the old column is dropped (which
660
+ * would drop it), an identity is dropped from the old column and added to
661
+ * the new one under the same sequence name, set to where the old one stood.
662
+ */
663
+ export function contractStatements(o: MigrationObservation, sequenceState?: SequenceState): string[] {
664
+ const n = o.names;
665
+ const t = n.qualifiedTable;
666
+ const old = o.old!;
667
+ const sequence = sequenceStatements(o, sequenceState, "contract");
668
+ const dflt = o.column.default ?? (o.sequence?.kind === "serial" ? old.default : undefined);
669
+ const out: string[] = [];
670
+ if (o.trigger) out.push(`DROP TRIGGER ${col(n.trigger)} ON ${t}`);
671
+ if (o.fn) out.push(`DROP FUNCTION ${n.qualifiedFn}()`);
672
+ out.push(...sequence.before);
673
+ out.push(`ALTER TABLE ${t} DROP COLUMN ${col(old.name)}`);
674
+ if (dflt !== undefined && n.change === "rename" && o.sequence?.kind !== "identity") out.push(`ALTER TABLE ${t} ALTER COLUMN ${col(n.column)} SET DEFAULT ${dflt}`);
675
+ out.push(...sequence.after);
676
+ return out;
677
+ }
678
+
543
679
  export async function contract(run: MigrationRun): Promise<ContractResult> {
544
680
  const plan = await retainPlan(run);
545
681
  if (!plan.oldColumn) return { state: plan.state, dropped: false };
@@ -550,9 +686,14 @@ export async function contract(run: MigrationRun): Promise<ContractResult> {
550
686
  const o = await observe(run);
551
687
  const n = o.names;
552
688
  await inTransaction(run, async (exec) => {
553
- if (o.trigger) await exec(`DROP TRIGGER ${col(n.trigger)} ON ${n.qualifiedTable}`);
554
- if (o.fn) await exec(`DROP FUNCTION ${n.qualifiedFn}()`);
555
- await exec(`ALTER TABLE ${n.qualifiedTable} DROP COLUMN ${col(o.old!.name)}`);
689
+ let state: SequenceState | undefined;
690
+ if (o.sequence?.kind === "identity" && n.change === "rename") {
691
+ // Writers wait from here, so the sequence's position read next is where the moved identity carries on.
692
+ await exec(`LOCK TABLE ${n.qualifiedTable} IN ACCESS EXCLUSIVE MODE`);
693
+ const [row] = await exec(`SELECT last_value::text AS last, is_called AS called FROM ${o.sequence.ident}`);
694
+ state = { lastValue: String(row?.last), isCalled: row?.called === true };
695
+ }
696
+ for (const sql of contractStatements(o, state)) await exec(sql);
556
697
  });
557
698
  await postgresReceiptStore(run.client, n.schema, identityOf(run.marker)).forget(batchPrefix(identityOf(run.marker), n.key));
558
699
  return { state: "done", dropped: true, oldColumn: plan.oldColumn };
@@ -619,7 +760,11 @@ export async function compensate(run: Pick<MigrationRun, "client" | "target" | "
619
760
  ).filter((c) => ours(c.comment, "carry"));
620
761
  const carriedIndexes = (
621
762
  await run.client.query<{ name: string; comment: string | null }>(
622
- "SELECT i.indexrelid::pg_catalog.regclass::text AS name, pg_catalog.obj_description(i.indexrelid, 'pg_class') AS comment FROM pg_catalog.pg_index i WHERE i.indrelid = $1::oid ORDER BY 1",
763
+ // The table's own indexes, and those on its partitions that no partitioned index has taken yet (an attached one goes with the partitioned index).
764
+ `SELECT i.indexrelid::pg_catalog.regclass::text AS name, pg_catalog.obj_description(i.indexrelid, 'pg_class') AS comment
765
+ FROM pg_catalog.pg_index i
766
+ WHERE (i.indrelid = $1::oid OR (i.indrelid IN (SELECT t.relid FROM pg_catalog.pg_partition_tree($1::oid::pg_catalog.regclass) t) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_inherits h WHERE h.inhrelid = i.indexrelid)))
767
+ ORDER BY i.indrelid <> $1::oid, 1`,
623
768
  [rel.oid],
624
769
  )
625
770
  ).filter((c) => ours(c.comment, "carry"));
@@ -643,7 +788,7 @@ export async function compensate(run: Pick<MigrationRun, "client" | "target" | "
643
788
  dropped.push(`constraint ${c.name} on ${c.rel}`);
644
789
  }
645
790
  for (const c of carriedIndexes) {
646
- await exec(`DROP INDEX ${c.name}`);
791
+ await exec(`DROP INDEX IF EXISTS ${c.name}`);
647
792
  dropped.push(`index ${c.name}`);
648
793
  }
649
794
  for (const c of newColumns) {
@@ -0,0 +1,141 @@
1
+ /**
2
+ * A table that is the target of a logical replication subscription (#4405),
3
+ * against two pinned servers with `wal_level = logical`, a publisher and a
4
+ * subscriber on one Docker network:
5
+ *
6
+ * - The fact the refusal rests on: a trigger made by plain `CREATE TRIGGER`
7
+ * does not fire for the subscription's apply worker, and one set
8
+ * `ENABLE ALWAYS` does.
9
+ * - The Plan phase refuses the subscriber's table, naming the subscription,
10
+ * and stops refusing it once the table is out of the publication and the
11
+ * subscription has refreshed.
12
+ *
13
+ * Needs Docker; skips cleanly without it.
14
+ */
15
+
16
+ import { execFile } from "node:child_process";
17
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { promisify } from "node:util";
21
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
22
+ import { dockerAvailable } from "../testing/server";
23
+ import { connectPostgres, type PostgresClient } from "../live/client";
24
+ import { postgresMigrationPlan, type PostgresMigrationDeps } from "../../op/activities/postgres-migration";
25
+ import { postgresImage, POSTGRES_LATEST_MAJOR } from "../../spec/postgres-pin";
26
+
27
+ const run = promisify(execFile);
28
+ const enabled = await dockerAvailable();
29
+ const PASSWORD = "chant";
30
+ const suffix = `${process.pid}-${Date.now()}`;
31
+ const network = `chant-sub-net-${suffix}`;
32
+ const dir = mkdtempSync(join(tmpdir(), "chant-pg-subscriber-"));
33
+ const MARKER = { stack: "e2e", env: "test" };
34
+
35
+ interface Server {
36
+ name: string;
37
+ port: string;
38
+ client: PostgresClient;
39
+ }
40
+ let pub: Server | undefined;
41
+ let sub: Server | undefined;
42
+
43
+ async function start(role: string): Promise<Server> {
44
+ const name = `chant-sub-${role}-${suffix}`;
45
+ await run("docker", [
46
+ "run", "-d", "--name", name, "--network", network, "-p", "127.0.0.1::5432", "-e", `POSTGRES_PASSWORD=${PASSWORD}`,
47
+ postgresImage(POSTGRES_LATEST_MAJOR), "-c", "wal_level=logical",
48
+ ]);
49
+ const port = (await run("docker", ["port", name, "5432/tcp"])).stdout.trim().split(":").pop()!;
50
+ for (let i = 0; i < 120; i++) {
51
+ try {
52
+ const client = await connectPostgres({ url: `postgres://postgres@127.0.0.1:${port}/postgres`, password: PASSWORD });
53
+ await client.query("SELECT 1");
54
+ return { name, port, client };
55
+ } catch {
56
+ await new Promise((r) => setTimeout(r, 1000));
57
+ }
58
+ }
59
+ throw new Error(`${name} did not accept a connection`);
60
+ }
61
+
62
+ beforeAll(async () => {
63
+ if (!enabled) return;
64
+ await run("docker", ["network", "create", network]);
65
+ [pub, sub] = await Promise.all([start("pub"), start("sub")]);
66
+ }, 600_000);
67
+
68
+ afterAll(async () => {
69
+ for (const s of [pub, sub]) {
70
+ await s?.client.end().catch(() => undefined);
71
+ if (s) await run("docker", ["rm", "-f", "-v", s.name]).catch(() => undefined);
72
+ }
73
+ if (enabled) await run("docker", ["network", "rm", network]).catch(() => undefined);
74
+ rmSync(dir, { recursive: true, force: true });
75
+ });
76
+
77
+ const waitFor = async (what: string, check: () => Promise<boolean>) => {
78
+ for (let i = 0; i < 60; i++) {
79
+ if (await check()) return;
80
+ await new Promise((r) => setTimeout(r, 500));
81
+ }
82
+ throw new Error(`timed out waiting for ${what}`);
83
+ };
84
+
85
+ const TABLE = "CREATE TABLE app.users (\n id bigint PRIMARY KEY,\n email text NOT NULL\n)";
86
+ const deps = (): PostgresMigrationDeps => ({
87
+ config: { ownership: MARKER, sql: { profiles: { e2e: { url: `postgres://postgres@127.0.0.1:${sub!.port}/postgres`, password: { env: "PG_E2E_PASSWORD" } } } } },
88
+ env: { PG_E2E_PASSWORD: PASSWORD },
89
+ log: () => undefined,
90
+ });
91
+ /** The Plan phase of a rename of email to login, on the subscriber. */
92
+ function planRename(): Promise<string> {
93
+ writeFileSync(
94
+ join(dir, "v2.json"),
95
+ JSON.stringify({
96
+ dialect: "postgres",
97
+ postgresMajor: 18,
98
+ applyOrder: ["app", "users"],
99
+ objects: [
100
+ { export: "app", type: "Postgres::Schema", ddl: "CREATE SCHEMA app", dependsOn: [] },
101
+ { export: "users", type: "Postgres::Table", dependsOn: ["app"], ddl: "CREATE TABLE app.users (\n id bigint PRIMARY KEY,\n login text NOT NULL -- previously: email\n)" },
102
+ ],
103
+ }),
104
+ );
105
+ return postgresMigrationPlan({ table: "app.users", column: "login", buildPath: "v2.json", environment: "e2e", stack: MARKER.stack, ownershipEnv: MARKER.env, cwd: dir } as never, undefined, deps()).then(
106
+ () => "",
107
+ (e: unknown) => String((e as Error).message),
108
+ );
109
+ }
110
+
111
+ describe.skipIf(!enabled)("a subscription's target table", () => {
112
+ test("the apply worker fires an ENABLE ALWAYS trigger and not a default one", async () => {
113
+ for (const s of [pub!, sub!]) await s.client.query(`CREATE SCHEMA app`).then(() => s.client.query(TABLE));
114
+ await pub!.client.query("CREATE PUBLICATION pub FOR TABLE app.users");
115
+ await sub!.client.query(`CREATE SUBSCRIPTION sub CONNECTION 'host=${pub!.name} port=5432 user=postgres password=${PASSWORD} dbname=postgres' PUBLICATION pub`);
116
+ await waitFor("the initial sync", async () => (await sub!.client.query<{ n: string }>("SELECT count(*)::text AS n FROM pg_subscription_rel WHERE srsubstate <> 'r'"))[0]!.n === "0");
117
+
118
+ await sub!.client.query("ALTER TABLE app.users ADD COLUMN seen text");
119
+ await sub!.client.query("CREATE FUNCTION app.mark() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.seen := TG_NAME; RETURN NEW; END $$");
120
+ await sub!.client.query("CREATE TRIGGER t_default BEFORE INSERT ON app.users FOR EACH ROW EXECUTE FUNCTION app.mark()");
121
+ await pub!.client.query("INSERT INTO app.users VALUES (1, 'a')");
122
+ await waitFor("row 1", async () => (await sub!.client.query("SELECT 1 FROM app.users WHERE id = 1")).length === 1);
123
+ expect(await sub!.client.query("SELECT seen FROM app.users WHERE id = 1")).toEqual([{ seen: null }]);
124
+
125
+ await sub!.client.query("ALTER TABLE app.users ENABLE ALWAYS TRIGGER t_default");
126
+ await pub!.client.query("INSERT INTO app.users VALUES (2, 'b')");
127
+ await waitFor("row 2", async () => (await sub!.client.query("SELECT 1 FROM app.users WHERE id = 2")).length === 1);
128
+ expect(await sub!.client.query("SELECT seen FROM app.users WHERE id = 2")).toEqual([{ seen: "t_default" }]);
129
+ await sub!.client.query("DROP TRIGGER t_default ON app.users");
130
+ await sub!.client.query("ALTER TABLE app.users DROP COLUMN seen");
131
+ }, 300_000);
132
+
133
+ test("the Plan phase refuses it naming the subscription, and not once the table has left the publication", async () => {
134
+ expect(await planRename()).toMatch(/target of logical replication subscription\(s\) sub\b/);
135
+
136
+ await pub!.client.query("ALTER PUBLICATION pub DROP TABLE app.users");
137
+ await sub!.client.query("ALTER SUBSCRIPTION sub REFRESH PUBLICATION");
138
+ await waitFor("the table to leave the subscription", async () => (await sub!.client.query("SELECT 1 FROM pg_subscription_rel")).length === 0);
139
+ expect(await planRename()).not.toMatch(/subscription/);
140
+ }, 300_000);
141
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The Plan phase's read of the subscriptions a table is the target of
3
+ * (#4405), and its refusal. The e2e is `subscriber.e2e.test.ts`.
4
+ */
5
+
6
+ import { describe, expect, test } from "vitest";
7
+ import type { PostgresClient } from "../live/client";
8
+ import { subscriptionRefusal, subscriptionsOf } from "./replication";
9
+
10
+ function fake(rows: unknown[]) {
11
+ const calls: { sql: string; params?: readonly unknown[] }[] = [];
12
+ const client = {
13
+ query: async (sql: string, params?: readonly unknown[]) => {
14
+ calls.push({ sql, params });
15
+ return rows;
16
+ },
17
+ } as unknown as Pick<PostgresClient, "query">;
18
+ return { client, calls };
19
+ }
20
+
21
+ describe("subscriptionsOf", () => {
22
+ test("reads pg_subscription_rel and pg_subscription for this database, by schema and table", async () => {
23
+ const { client, calls } = fake([{ subname: "sub_a", subenabled: true }, { subname: "sub_b", subenabled: false }]);
24
+ expect(await subscriptionsOf(client, "app", "users", 18)).toEqual([{ name: "sub_a", enabled: true }, { name: "sub_b", enabled: false }]);
25
+ expect(calls).toHaveLength(1);
26
+ expect(calls[0]!.params).toEqual(["app", "users"]);
27
+ expect(calls[0]!.sql).toContain("pg_catalog.pg_subscription_rel");
28
+ expect(calls[0]!.sql).toContain("pg_catalog.pg_subscription s");
29
+ expect(calls[0]!.sql).toContain("s.subdbid");
30
+ });
31
+
32
+ test("a partitioned table is matched through its partitions; before 12 the table alone", async () => {
33
+ const now = fake([]);
34
+ await subscriptionsOf(now.client, "app", "events", 18);
35
+ expect(now.calls[0]!.sql).toContain("pg_partition_tree");
36
+ const old = fake([]);
37
+ await subscriptionsOf(old.client, "app", "events", 11);
38
+ expect(old.calls[0]!.sql).not.toContain("pg_partition_tree");
39
+ });
40
+
41
+ test("no row means no subscription", async () => {
42
+ expect(await subscriptionsOf(fake([]).client, "app", "users", 18)).toEqual([]);
43
+ });
44
+ });
45
+
46
+ describe("subscriptionRefusal", () => {
47
+ const message = subscriptionRefusal("app.users", "login", [{ name: "sub_a", enabled: true }, { name: "sub_b", enabled: false }]);
48
+
49
+ test("names each subscription, and a disabled one as such", () => {
50
+ expect(message).toContain("sub_a, sub_b (disabled)");
51
+ expect(message.startsWith("app.users is a target")).toBe(true);
52
+ });
53
+
54
+ test("gives the reason and the way out", () => {
55
+ expect(message).toContain("session_replication_role = replica");
56
+ expect(message).toContain("ENABLE ALWAYS");
57
+ expect(message).toContain("REFRESH PUBLICATION");
58
+ });
59
+ });