@intentius/chant-lexicon-sql 0.124.45 → 0.124.48

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 (360) 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/stream.d.ts +40 -0
  37. package/dist/clickhouse/plan/stream.d.ts.map +1 -0
  38. package/dist/clickhouse/plan/stream.js +107 -0
  39. package/dist/clickhouse/rebuild/observe.d.ts +3 -1
  40. package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
  41. package/dist/clickhouse/rebuild/observe.js +14 -1
  42. package/dist/clickhouse/review-catalog.d.ts +213 -0
  43. package/dist/clickhouse/review-catalog.d.ts.map +1 -0
  44. package/dist/clickhouse/review-catalog.js +330 -0
  45. package/dist/codegen/classifier-page.d.ts.map +1 -1
  46. package/dist/codegen/classifier-page.js +21 -4
  47. package/dist/codegen/clickhouse-module.d.ts.map +1 -1
  48. package/dist/codegen/clickhouse-module.js +3 -0
  49. package/dist/codegen/coverage-page.d.ts.map +1 -1
  50. package/dist/codegen/coverage-page.js +5 -3
  51. package/dist/codegen/docs.js +1 -1
  52. package/dist/codegen/postgres-classifier-page.js +1 -1
  53. package/dist/codegen/registry.d.ts.map +1 -1
  54. package/dist/codegen/registry.js +5 -0
  55. package/dist/composites/catalog.d.ts.map +1 -1
  56. package/dist/composites/catalog.js +100 -0
  57. package/dist/composites/clickhouse.d.ts +2 -0
  58. package/dist/composites/clickhouse.d.ts.map +1 -1
  59. package/dist/composites/clickhouse.js +2 -0
  60. package/dist/composites/otel-tables.d.ts +100 -0
  61. package/dist/composites/otel-tables.d.ts.map +1 -0
  62. package/dist/composites/otel-tables.js +282 -0
  63. package/dist/composites/stream-ingest.d.ts +63 -0
  64. package/dist/composites/stream-ingest.d.ts.map +1 -0
  65. package/dist/composites/stream-ingest.js +39 -0
  66. package/dist/coverage/clickhouse.json +1 -1
  67. package/dist/coverage/postgres.json +7 -7
  68. package/dist/coverage.d.ts +16 -6
  69. package/dist/coverage.d.ts.map +1 -1
  70. package/dist/coverage.js +10 -5
  71. package/dist/drizzle-command.d.ts +17 -0
  72. package/dist/drizzle-command.d.ts.map +1 -0
  73. package/dist/drizzle-command.js +91 -0
  74. package/dist/drizzle.d.ts +70 -0
  75. package/dist/drizzle.d.ts.map +1 -0
  76. package/dist/drizzle.js +684 -0
  77. package/dist/files/clickhouse.d.ts.map +1 -1
  78. package/dist/files/clickhouse.js +8 -1
  79. package/dist/files/common.d.ts +13 -0
  80. package/dist/files/common.d.ts.map +1 -1
  81. package/dist/files/common.js +5 -0
  82. package/dist/files/postgres.js +12 -2
  83. package/dist/generated/clickhouse.d.ts +2 -0
  84. package/dist/generated/clickhouse.d.ts.map +1 -1
  85. package/dist/generated/clickhouse.js +2 -0
  86. package/dist/import-parser.d.ts +21 -1
  87. package/dist/import-parser.d.ts.map +1 -1
  88. package/dist/import-parser.js +79 -5
  89. package/dist/index.d.ts +1 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +1 -1
  92. package/dist/integrity.json +6 -5
  93. package/dist/lint/post-synth/audit-entries.d.ts.map +1 -1
  94. package/dist/lint/post-synth/audit-entries.js +1 -0
  95. package/dist/lint/post-synth/index.d.ts.map +1 -1
  96. package/dist/lint/post-synth/index.js +2 -0
  97. package/dist/lint/post-synth/sqlch128.d.ts +18 -0
  98. package/dist/lint/post-synth/sqlch128.d.ts.map +1 -0
  99. package/dist/lint/post-synth/sqlch128.js +222 -0
  100. package/dist/lint/rules/postgres-templates.js +2 -2
  101. package/dist/lsp/postgres-template.d.ts.map +1 -1
  102. package/dist/lsp/postgres-template.js +11 -0
  103. package/dist/manifest.json +1 -1
  104. package/dist/mcp/postgres.js +1 -1
  105. package/dist/meta.json +25 -0
  106. package/dist/migrate/from-postgres/detect.d.ts +4 -0
  107. package/dist/migrate/from-postgres/detect.d.ts.map +1 -0
  108. package/dist/migrate/from-postgres/detect.js +7 -0
  109. package/dist/migrate/from-postgres/emit.d.ts +26 -0
  110. package/dist/migrate/from-postgres/emit.d.ts.map +1 -0
  111. package/dist/migrate/from-postgres/emit.js +86 -0
  112. package/dist/migrate/from-postgres/index.d.ts +51 -0
  113. package/dist/migrate/from-postgres/index.d.ts.map +1 -0
  114. package/dist/migrate/from-postgres/index.js +115 -0
  115. package/dist/migrate/from-postgres/translate.d.ts +73 -0
  116. package/dist/migrate/from-postgres/translate.d.ts.map +1 -0
  117. package/dist/migrate/from-postgres/translate.js +261 -0
  118. package/dist/migrate/from-postgres/types.d.ts +51 -0
  119. package/dist/migrate/from-postgres/types.d.ts.map +1 -0
  120. package/dist/migrate/from-postgres/types.js +211 -0
  121. package/dist/migration-replay.d.ts +73 -0
  122. package/dist/migration-replay.d.ts.map +1 -0
  123. package/dist/migration-replay.js +294 -0
  124. package/dist/okf/index.md +6 -0
  125. package/dist/okf/rules/SQLCH128.md +11 -0
  126. package/dist/okf/types/PostgresForeignDataWrapper.md +9 -0
  127. package/dist/okf/types/PostgresForeignServer.md +9 -0
  128. package/dist/okf/types/PostgresOperator.md +9 -0
  129. package/dist/okf/types/PostgresPublication.md +9 -0
  130. package/dist/okf/types/PostgresStatistics.md +9 -0
  131. package/dist/op/activities/postgres-migration.d.ts.map +1 -1
  132. package/dist/op/activities/postgres-migration.js +4 -0
  133. package/dist/op/activity-contracts.d.ts +20 -0
  134. package/dist/op/activity-contracts.d.ts.map +1 -1
  135. package/dist/op/activity-contracts.js +2 -0
  136. package/dist/plugin.d.ts.map +1 -1
  137. package/dist/plugin.js +36 -2
  138. package/dist/postgres/apply/apply.js +8 -1
  139. package/dist/postgres/apply/statements.d.ts.map +1 -1
  140. package/dist/postgres/apply/statements.js +87 -2
  141. package/dist/postgres/constraint-names.d.ts +30 -0
  142. package/dist/postgres/constraint-names.d.ts.map +1 -0
  143. package/dist/postgres/constraint-names.js +72 -0
  144. package/dist/postgres/entities.d.ts +110 -1
  145. package/dist/postgres/entities.d.ts.map +1 -1
  146. package/dist/postgres/entities.js +146 -0
  147. package/dist/postgres/entity-types.d.ts +5 -0
  148. package/dist/postgres/entity-types.d.ts.map +1 -1
  149. package/dist/postgres/entity-types.js +5 -0
  150. package/dist/postgres/import/generator.d.ts.map +1 -1
  151. package/dist/postgres/import/generator.js +50 -2
  152. package/dist/postgres/import/live-export.d.ts.map +1 -1
  153. package/dist/postgres/import/live-export.js +2 -0
  154. package/dist/postgres/live/catalog-kinds.d.ts +14 -0
  155. package/dist/postgres/live/catalog-kinds.d.ts.map +1 -1
  156. package/dist/postgres/live/catalog-kinds.js +206 -0
  157. package/dist/postgres/live/catalog.d.ts +2 -0
  158. package/dist/postgres/live/catalog.d.ts.map +1 -1
  159. package/dist/postgres/live/catalog.js +3 -1
  160. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  161. package/dist/postgres/live/describe-resources.js +10 -2
  162. package/dist/postgres/migrate/backfill.d.ts +2 -0
  163. package/dist/postgres/migrate/backfill.d.ts.map +1 -1
  164. package/dist/postgres/migrate/backfill.js +7 -1
  165. package/dist/postgres/migrate/carry.d.ts +45 -1
  166. package/dist/postgres/migrate/carry.d.ts.map +1 -1
  167. package/dist/postgres/migrate/carry.js +72 -8
  168. package/dist/postgres/migrate/observe.d.ts +28 -4
  169. package/dist/postgres/migrate/observe.d.ts.map +1 -1
  170. package/dist/postgres/migrate/observe.js +41 -12
  171. package/dist/postgres/migrate/op.d.ts +26 -1
  172. package/dist/postgres/migrate/op.d.ts.map +1 -1
  173. package/dist/postgres/migrate/op.js +3 -1
  174. package/dist/postgres/migrate/steps.d.ts +35 -2
  175. package/dist/postgres/migrate/steps.d.ts.map +1 -1
  176. package/dist/postgres/migrate/steps.js +158 -12
  177. package/dist/postgres/parser.d.ts +72 -1
  178. package/dist/postgres/parser.d.ts.map +1 -1
  179. package/dist/postgres/parser.js +194 -2
  180. package/dist/postgres/plan/deep.d.ts.map +1 -1
  181. package/dist/postgres/plan/deep.js +29 -1
  182. package/dist/postgres/plan/diff.d.ts.map +1 -1
  183. package/dist/postgres/plan/diff.js +39 -1
  184. package/dist/postgres/plan/normalize.d.ts +17 -2
  185. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  186. package/dist/postgres/plan/normalize.js +109 -1
  187. package/dist/postgres/plan/rules.d.ts +1 -0
  188. package/dist/postgres/plan/rules.d.ts.map +1 -1
  189. package/dist/postgres/plan/rules.js +3 -2
  190. package/dist/postgres/plan/schema.js +1 -1
  191. package/dist/postgres/secret-options.d.ts +10 -0
  192. package/dist/postgres/secret-options.d.ts.map +1 -0
  193. package/dist/postgres/secret-options.js +9 -0
  194. package/dist/postgres/testing/roundtrip-schema.d.ts +3 -2
  195. package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
  196. package/dist/postgres/testing/roundtrip-schema.js +13 -1
  197. package/dist/postgres.d.ts +1 -1
  198. package/dist/postgres.d.ts.map +1 -1
  199. package/dist/postgres.js +1 -1
  200. package/dist/rules/audit-entries.ts +1 -0
  201. package/dist/rules/postgres-templates.ts +2 -2
  202. package/dist/rules/sqlch128.ts +230 -0
  203. package/dist/serializer.d.ts +5 -0
  204. package/dist/serializer.d.ts.map +1 -1
  205. package/dist/serializer.js +13 -2
  206. package/dist/sql-types-command.d.ts +13 -0
  207. package/dist/sql-types-command.d.ts.map +1 -1
  208. package/dist/sql-types-command.js +31 -16
  209. package/dist/sql-types.d.ts +22 -0
  210. package/dist/sql-types.d.ts.map +1 -1
  211. package/dist/sql-types.js +10 -6
  212. package/dist/sqlserver/lexer.d.ts +37 -0
  213. package/dist/sqlserver/lexer.d.ts.map +1 -0
  214. package/dist/sqlserver/lexer.js +254 -0
  215. package/dist/sqlserver/parse.d.ts +185 -0
  216. package/dist/sqlserver/parse.d.ts.map +1 -0
  217. package/dist/sqlserver/parse.js +767 -0
  218. package/dist/sqlserver/translate.d.ts +34 -0
  219. package/dist/sqlserver/translate.d.ts.map +1 -0
  220. package/dist/sqlserver/translate.js +495 -0
  221. package/dist/sqlserver/types.d.ts +44 -0
  222. package/dist/sqlserver/types.d.ts.map +1 -0
  223. package/dist/sqlserver/types.js +133 -0
  224. package/dist/sqlserver/validate.d.ts +31 -0
  225. package/dist/sqlserver/validate.d.ts.map +1 -0
  226. package/dist/sqlserver/validate.js +67 -0
  227. package/dist/validate.d.ts.map +1 -1
  228. package/dist/validate.js +5 -0
  229. package/package.json +2 -2
  230. package/src/change-set.ts +2 -1
  231. package/src/clickhouse/apply/statements.ts +25 -0
  232. package/src/clickhouse/import/live-export.ts +20 -5
  233. package/src/clickhouse/live/named-collections.e2e.test.ts +88 -0
  234. package/src/clickhouse/live/named-collections.ts +61 -0
  235. package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -3
  236. package/src/clickhouse/live/undeclarable.ts +20 -4
  237. package/src/clickhouse/named-collections.test.ts +187 -0
  238. package/src/clickhouse/named-collections.ts +224 -0
  239. package/src/clickhouse/parser.ts +2 -1
  240. package/src/clickhouse/plan/commands.ts +17 -2
  241. package/src/clickhouse/plan/diff.ts +57 -1
  242. package/src/clickhouse/plan/disruption.ts +5 -0
  243. package/src/clickhouse/plan/rebuild-handoff.ts +3 -0
  244. package/src/clickhouse/plan/report.ts +6 -1
  245. package/src/clickhouse/plan/rules.ts +14 -4
  246. package/src/clickhouse/plan/stream-s3queue.e2e.test.ts +148 -0
  247. package/src/clickhouse/plan/stream.test.ts +237 -0
  248. package/src/clickhouse/plan/stream.ts +111 -0
  249. package/src/clickhouse/rebuild/observe.ts +15 -1
  250. package/src/clickhouse/review-catalog.test.ts +181 -0
  251. package/src/clickhouse/review-catalog.ts +489 -0
  252. package/src/codegen/classifier-page.ts +21 -4
  253. package/src/codegen/clickhouse-module.ts +3 -0
  254. package/src/codegen/coverage-page.ts +5 -3
  255. package/src/codegen/docs.ts +1 -1
  256. package/src/codegen/postgres-classifier-page.ts +1 -1
  257. package/src/codegen/registry.ts +5 -0
  258. package/src/composites/catalog.test.ts +6 -1
  259. package/src/composites/catalog.ts +100 -0
  260. package/src/composites/clickhouse.ts +2 -0
  261. package/src/composites/composites.test.ts +1 -1
  262. package/src/composites/otel-tables.test.ts +118 -0
  263. package/src/composites/otel-tables.ts +339 -0
  264. package/src/composites/stream-ingest.test.ts +114 -0
  265. package/src/composites/stream-ingest.ts +82 -0
  266. package/src/core/undeclarable.test.ts +6 -5
  267. package/src/coverage/clickhouse.json +1 -1
  268. package/src/coverage/postgres.json +7 -7
  269. package/src/coverage.test.ts +27 -2
  270. package/src/coverage.ts +18 -7
  271. package/src/drizzle-command.ts +94 -0
  272. package/src/drizzle-kit.e2e.test.ts +89 -0
  273. package/src/drizzle-types.e2e.test.ts +67 -0
  274. package/src/drizzle.test.ts +358 -0
  275. package/src/drizzle.ts +737 -0
  276. package/src/files/clickhouse.ts +8 -1
  277. package/src/files/common.ts +14 -1
  278. package/src/files/postgres.ts +12 -2
  279. package/src/generated/clickhouse.ts +3 -0
  280. package/src/generated/lexicon-sql.json +25 -0
  281. package/src/import-parser.sqlserver.test.ts +92 -0
  282. package/src/import-parser.ts +79 -5
  283. package/src/index.ts +1 -1
  284. package/src/lint/post-synth/audit-entries.ts +1 -0
  285. package/src/lint/post-synth/index.ts +2 -0
  286. package/src/lint/post-synth/sqlch128.test.ts +107 -0
  287. package/src/lint/post-synth/sqlch128.ts +230 -0
  288. package/src/lint/rules/postgres-templates.ts +2 -2
  289. package/src/lsp/postgres-template.ts +11 -0
  290. package/src/mcp/postgres.ts +1 -1
  291. package/src/migrate/from-postgres/cli.test.ts +165 -0
  292. package/src/migrate/from-postgres/detect.ts +7 -0
  293. package/src/migrate/from-postgres/emit.ts +96 -0
  294. package/src/migrate/from-postgres/example.test.ts +205 -0
  295. package/src/migrate/from-postgres/index.ts +122 -0
  296. package/src/migrate/from-postgres/translate.test.ts +242 -0
  297. package/src/migrate/from-postgres/translate.ts +359 -0
  298. package/src/migrate/from-postgres/types.test.ts +116 -0
  299. package/src/migrate/from-postgres/types.ts +236 -0
  300. package/src/migrate/from-postgres/validate.e2e.test.ts +44 -0
  301. package/src/migrate/from-postgres/workspace.e2e.test.ts +114 -0
  302. package/src/migration-replay.e2e.test.ts +155 -0
  303. package/src/migration-replay.test.ts +173 -0
  304. package/src/migration-replay.ts +289 -0
  305. package/src/op/__snapshots__/watch-pipeline.test.ts.snap +2 -2
  306. package/src/op/activities/postgres-migration.ts +4 -0
  307. package/src/op/activity-contracts.ts +2 -0
  308. package/src/plugin.ts +37 -2
  309. package/src/postgres/apply/apply.ts +9 -1
  310. package/src/postgres/apply/statements.ts +74 -1
  311. package/src/postgres/constraint-names.ts +71 -0
  312. package/src/postgres/database-kinds.test.ts +384 -0
  313. package/src/postgres/entities.ts +266 -1
  314. package/src/postgres/entity-types.ts +5 -0
  315. package/src/postgres/import/generator.ts +42 -2
  316. package/src/postgres/import/live-export.ts +1 -0
  317. package/src/postgres/live/catalog-kinds.ts +223 -0
  318. package/src/postgres/live/catalog.ts +6 -1
  319. package/src/postgres/live/database-kinds.e2e.test.ts +164 -0
  320. package/src/postgres/live/describe-resources.ts +11 -2
  321. package/src/postgres/live/undeclarable.e2e.test.ts +13 -6
  322. package/src/postgres/migrate/backfill.ts +13 -1
  323. package/src/postgres/migrate/carry.ts +103 -8
  324. package/src/postgres/migrate/followups.e2e.test.ts +218 -0
  325. package/src/postgres/migrate/followups.test.ts +199 -0
  326. package/src/postgres/migrate/kinds.e2e.test.ts +5 -5
  327. package/src/postgres/migrate/observe.ts +53 -14
  328. package/src/postgres/migrate/op.ts +28 -1
  329. package/src/postgres/migrate/partition.e2e.test.ts +129 -0
  330. package/src/postgres/migrate/partition.test.ts +206 -0
  331. package/src/postgres/migrate/steps.ts +157 -12
  332. package/src/postgres/parser.ts +234 -2
  333. package/src/postgres/plan/deep.ts +28 -1
  334. package/src/postgres/plan/diff.ts +36 -1
  335. package/src/postgres/plan/normalize.ts +122 -1
  336. package/src/postgres/plan/rules.ts +10 -3
  337. package/src/postgres/plan/schema.ts +1 -1
  338. package/src/postgres/secret-options.ts +10 -0
  339. package/src/postgres/testing/roundtrip-schema.ts +13 -1
  340. package/src/postgres.ts +16 -0
  341. package/src/serializer.ts +13 -2
  342. package/src/skills/chant-sql-postgres-migration.md +3 -3
  343. package/src/skills/chant-sql-postgres.md +6 -1
  344. package/src/skills/chant-sql.md +2 -1
  345. package/src/sql-types-command.ts +38 -16
  346. package/src/sql-types.ts +11 -6
  347. package/src/sqlserver/fixture.test.ts +186 -0
  348. package/src/sqlserver/import-cli.test.ts +61 -0
  349. package/src/sqlserver/lexer.ts +260 -0
  350. package/src/sqlserver/parse.ts +865 -0
  351. package/src/sqlserver/translate.test.ts +396 -0
  352. package/src/sqlserver/translate.ts +558 -0
  353. package/src/sqlserver/types.test.ts +102 -0
  354. package/src/sqlserver/types.ts +154 -0
  355. package/src/sqlserver/validate.e2e.test.ts +32 -0
  356. package/src/sqlserver/validate.test.ts +49 -0
  357. package/src/sqlserver/validate.ts +76 -0
  358. package/src/validate.ts +5 -0
  359. package/src/workspace-kinds.test.ts +11 -0
  360. package/workspace-kinds.json +100 -2
@@ -23,6 +23,7 @@ import { notExtensionOwned, scopeSql, type LivePgObject, type SchemaScope } from
23
23
  import { POSTGRES_ENTITY_TYPES } from "../entity-types";
24
24
  import { quoteIdent } from "../keywords";
25
25
  import { stripMarker } from "../../core/ownership";
26
+ import { isSecretOption } from "../secret-options";
26
27
 
27
28
  type Row = Record<string, unknown>;
28
29
 
@@ -310,3 +311,225 @@ export async function readRoutineKinds(client: PostgresClient, scope: SchemaScop
310
311
  }
311
312
  return out;
312
313
  }
314
+
315
+ /** An operator named by another's COMMUTATOR or NEGATOR, as `OPERATOR(schema.symbol)`. */
316
+ const operatorRef = (column: string): string =>
317
+ `(SELECT 'OPERATOR(' || pg_catalog.quote_ident(rn.nspname) || '.' || r.oprname || ')' FROM pg_catalog.pg_operator r JOIN pg_catalog.pg_namespace rn ON rn.oid = r.oprnamespace WHERE r.oid = ${column})`;
318
+
319
+ /**
320
+ * Operators: the ones a schema defines, not the server's own (oids below the
321
+ * first normal object id) or an extension's. Identified by name and operand
322
+ * types, `NONE` for a missing side (`DROP OPERATOR`'s spelling).
323
+ */
324
+ export async function readOperators(client: PostgresClient, scope: SchemaScope): Promise<LivePgObject[]> {
325
+ const s = scopeSql("n.nspname", scope);
326
+ const rows = await client.query<Row>(
327
+ `SELECT o.oid::text AS oid, n.nspname AS schema, o.oprname AS name,
328
+ CASE WHEN o.oprleft <> 0 THEN pg_catalog.format_type(o.oprleft, NULL) END AS leftarg,
329
+ CASE WHEN o.oprright <> 0 THEN pg_catalog.format_type(o.oprright, NULL) END AS rightarg,
330
+ ${proc("o.oprcode::pg_catalog.regproc")} AS func, ${operatorRef("o.oprcom")} AS commutator, ${operatorRef("o.oprnegate")} AS negator,
331
+ ${proc("o.oprrest::pg_catalog.regproc")} AS restrict, ${proc("o.oprjoin::pg_catalog.regproc")} AS join_func,
332
+ o.oprcanhash AS hashes, o.oprcanmerge AS merges,
333
+ pg_catalog.obj_description(o.oid, 'pg_operator') AS comment, ${dependentsOf("pg_operator", "o.oid")} AS dependents
334
+ FROM pg_catalog.pg_operator o JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace
335
+ WHERE ${s.sql} AND o.oid >= 16384 AND ${notExtensionOwned("pg_operator", "o.oid")}
336
+ ORDER BY n.nspname ${C}, o.oprname ${C}, pg_catalog.format_type(o.oprleft, NULL) ${C} NULLS FIRST, pg_catalog.format_type(o.oprright, NULL) ${C} NULLS FIRST`,
337
+ s.params,
338
+ );
339
+ return rows.map((r) => {
340
+ const left = str(r.leftarg);
341
+ const right = str(r.rightarg);
342
+ const signature = `(${left ?? "NONE"},${right ?? "NONE"})`;
343
+ const name = `${quoteIdent(String(r.schema))}.${r.name}`;
344
+ const opts = [`FUNCTION = ${r.func}`];
345
+ if (left) opts.push(`LEFTARG = ${left}`);
346
+ if (right) opts.push(`RIGHTARG = ${right}`);
347
+ if (str(r.commutator)) opts.push(`COMMUTATOR = ${r.commutator}`);
348
+ if (str(r.negator)) opts.push(`NEGATOR = ${r.negator}`);
349
+ if (str(r.restrict)) opts.push(`RESTRICT = ${r.restrict}`);
350
+ if (str(r.join_func)) opts.push(`JOIN = ${r.join_func}`);
351
+ if (r.hashes === true) opts.push("HASHES");
352
+ if (r.merges === true) opts.push("MERGES");
353
+ return {
354
+ type: POSTGRES_ENTITY_TYPES.operator,
355
+ schema: String(r.schema),
356
+ name: String(r.name),
357
+ oid: String(r.oid),
358
+ signature,
359
+ ...withComment(r),
360
+ ...dependents(r),
361
+ statement: [`CREATE OPERATOR ${name} (${opts.join(", ")})`, ...commentOn(`OPERATOR ${name}${signature}`, r.comment)].join(";\n"),
362
+ };
363
+ });
364
+ }
365
+
366
+ /** An `OPTIONS (...)` clause from a catalog `text[]` of `name=value`, and the names left out because they hold a credential. */
367
+ function optionsOf(raw: unknown): { clause: string; omitted: string[] } {
368
+ const kept: string[] = [];
369
+ const omitted: string[] = [];
370
+ for (const entry of (raw as string[] | null) ?? []) {
371
+ const at = entry.indexOf("=");
372
+ const name = at < 0 ? entry : entry.slice(0, at);
373
+ if (isSecretOption(name)) omitted.push(name);
374
+ else kept.push(`${quoteIdent(name)} ${literal(at < 0 ? "" : entry.slice(at + 1))}`);
375
+ }
376
+ return { clause: kept.length > 0 ? ` OPTIONS (${kept.join(", ")})` : "", omitted };
377
+ }
378
+
379
+ const omittedWarning = (omitted: readonly string[]): { warnings?: string[] } =>
380
+ omitted.length > 0 ? { warnings: [`the option${omitted.length === 1 ? "" : "s"} ${omitted.join(", ")} hold${omitted.length === 1 ? "s" : ""} a credential and ${omitted.length === 1 ? "is" : "are"} left out; the environment supplies it`] } : {};
381
+
382
+ /**
383
+ * The kinds of object #3911 adds that belong to the database or hang off a
384
+ * table: extended statistics (printed by the server), foreign data wrappers
385
+ * and servers, and publications, each assembled from its catalog rows. A
386
+ * wrapper, server or publication is the database's, so the schema scope does
387
+ * not narrow it. A credential in an option is left out and said so.
388
+ */
389
+ export async function readDatabaseKinds(client: PostgresClient, scope: SchemaScope): Promise<LivePgObject[]> {
390
+ const out: LivePgObject[] = [...(await readOperators(client, scope))];
391
+
392
+ const s = scopeSql("n.nspname", scope);
393
+ const stats = await client.query<Row>(
394
+ `SELECT e.oid::text AS oid, n.nspname AS schema, e.stxname AS name, pg_catalog.pg_get_statisticsobjdef(e.oid) AS def,
395
+ pg_catalog.obj_description(e.oid, 'pg_statistic_ext') AS comment
396
+ FROM pg_catalog.pg_statistic_ext e JOIN pg_catalog.pg_namespace n ON n.oid = e.stxnamespace
397
+ WHERE ${s.sql} AND ${notExtensionOwned("pg_statistic_ext", "e.oid")}
398
+ ORDER BY n.nspname ${C}, e.stxname ${C}`,
399
+ s.params,
400
+ );
401
+ for (const r of stats) {
402
+ const q = qname(String(r.schema), String(r.name));
403
+ out.push({
404
+ type: POSTGRES_ENTITY_TYPES.statistics,
405
+ schema: String(r.schema),
406
+ name: String(r.name),
407
+ oid: String(r.oid),
408
+ ...withComment(r),
409
+ statement: [String(r.def), ...commentOn(`STATISTICS ${q}`, r.comment)].join(";\n"),
410
+ });
411
+ }
412
+
413
+ const wrappers = await client.query<Row>(
414
+ `SELECT w.oid::text AS oid, w.fdwname AS name, ${proc("w.fdwhandler::pg_catalog.regproc")} AS handler, ${proc("w.fdwvalidator::pg_catalog.regproc")} AS validator,
415
+ w.fdwoptions AS options, pg_catalog.obj_description(w.oid, 'pg_foreign_data_wrapper') AS comment, ${dependentsOf("pg_foreign_data_wrapper", "w.oid")} AS dependents
416
+ FROM pg_catalog.pg_foreign_data_wrapper w
417
+ WHERE ${notExtensionOwned("pg_foreign_data_wrapper", "w.oid")}
418
+ ORDER BY w.fdwname ${C}`,
419
+ );
420
+ for (const r of wrappers) {
421
+ const name = quoteIdent(String(r.name));
422
+ const { clause, omitted } = optionsOf(r.options);
423
+ out.push({
424
+ type: POSTGRES_ENTITY_TYPES.foreignDataWrapper,
425
+ name: String(r.name),
426
+ oid: String(r.oid),
427
+ ...withComment(r),
428
+ ...dependents(r),
429
+ ...omittedWarning(omitted),
430
+ statement: [
431
+ `CREATE FOREIGN DATA WRAPPER ${name}${str(r.handler) ? ` HANDLER ${r.handler}` : ""}${str(r.validator) ? ` VALIDATOR ${r.validator}` : ""}${clause}`,
432
+ ...commentOn(`FOREIGN DATA WRAPPER ${name}`, r.comment),
433
+ ].join(";\n"),
434
+ });
435
+ }
436
+
437
+ const servers = await client.query<Row>(
438
+ `SELECT v.oid::text AS oid, v.srvname AS name, v.srvtype AS type, v.srvversion AS version, w.fdwname AS wrapper, v.srvoptions AS options,
439
+ pg_catalog.obj_description(v.oid, 'pg_foreign_server') AS comment, ${dependentsOf("pg_foreign_server", "v.oid")} AS dependents
440
+ FROM pg_catalog.pg_foreign_server v JOIN pg_catalog.pg_foreign_data_wrapper w ON w.oid = v.srvfdw
441
+ WHERE ${notExtensionOwned("pg_foreign_server", "v.oid")}
442
+ ORDER BY v.srvname ${C}`,
443
+ );
444
+ for (const r of servers) {
445
+ const name = quoteIdent(String(r.name));
446
+ const { clause, omitted } = optionsOf(r.options);
447
+ out.push({
448
+ type: POSTGRES_ENTITY_TYPES.foreignServer,
449
+ name: String(r.name),
450
+ oid: String(r.oid),
451
+ ...withComment(r),
452
+ ...dependents(r),
453
+ ...omittedWarning(omitted),
454
+ statement: [
455
+ `CREATE SERVER ${name}${str(r.type) ? ` TYPE ${literal(String(r.type))}` : ""}${str(r.version) ? ` VERSION ${literal(String(r.version))}` : ""} FOREIGN DATA WRAPPER ${quoteIdent(String(r.wrapper))}${clause}`,
456
+ ...commentOn(`SERVER ${name}`, r.comment),
457
+ ].join(";\n"),
458
+ });
459
+ }
460
+
461
+ out.push(...(await readPublications(client)));
462
+ return out;
463
+ }
464
+
465
+ /** Publications. Column lists, row filters and schema members are 15's; they are read where the catalog has them. */
466
+ async function readPublications(client: PostgresClient): Promise<LivePgObject[]> {
467
+ const has = async (catalog: string, column?: string): Promise<boolean> => {
468
+ const rows = await client.query<Row>(
469
+ column
470
+ ? `SELECT 1 AS x FROM pg_catalog.pg_attribute WHERE attrelid = to_regclass('pg_catalog.${catalog}') AND attname = '${column}' AND NOT attisdropped`
471
+ : `SELECT 1 AS x WHERE to_regclass('pg_catalog.${catalog}') IS NOT NULL`,
472
+ );
473
+ return rows.length > 0;
474
+ };
475
+ const columnLists = await has("pg_publication_rel", "prattrs");
476
+ const schemaMembers = await has("pg_publication_namespace");
477
+
478
+ const pubs = await client.query<Row>(
479
+ `SELECT p.oid::text AS oid, p.pubname AS name, p.puballtables AS alltables, p.pubinsert, p.pubupdate, p.pubdelete, p.pubtruncate,
480
+ pg_catalog.to_jsonb(p) AS def, pg_catalog.obj_description(p.oid, 'pg_publication') AS comment
481
+ FROM pg_catalog.pg_publication p
482
+ WHERE ${notExtensionOwned("pg_publication", "p.oid")}
483
+ ORDER BY p.pubname ${C}`,
484
+ );
485
+ const out: LivePgObject[] = [];
486
+ for (const r of pubs) {
487
+ const name = quoteIdent(String(r.name));
488
+ const d = (typeof r.def === "string" ? JSON.parse(r.def) : r.def) as Record<string, unknown>;
489
+ const tables = r.alltables
490
+ ? []
491
+ : await client.query<Row>(
492
+ `SELECT n.nspname AS schema, c.relname AS name,
493
+ ${columnLists ? "ARRAY(SELECT pg_catalog.quote_ident(a.attname) FROM pg_catalog.pg_attribute a WHERE a.attrelid = r.prrelid AND a.attnum = ANY(r.prattrs::pg_catalog.int2[]) ORDER BY a.attnum)" : "NULL::text[]"} AS columns,
494
+ ${columnLists ? "pg_catalog.pg_get_expr(r.prqual, r.prrelid)" : "NULL::text"} AS filter
495
+ FROM pg_catalog.pg_publication_rel r JOIN pg_catalog.pg_class c ON c.oid = r.prrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
496
+ WHERE r.prpubid = $1::oid ORDER BY n.nspname ${C}, c.relname ${C}`,
497
+ [String(r.oid)],
498
+ );
499
+ const schemas =
500
+ schemaMembers && !r.alltables
501
+ ? await client.query<Row>(
502
+ `SELECT n.nspname AS name FROM pg_catalog.pg_publication_namespace pn JOIN pg_catalog.pg_namespace n ON n.oid = pn.pnnspid WHERE pn.pnpubid = $1::oid ORDER BY n.nspname ${C}`,
503
+ [String(r.oid)],
504
+ )
505
+ : [];
506
+ const members = [
507
+ ...tables.map((t) => {
508
+ const columns = (t.columns as string[] | null) ?? [];
509
+ return `${qname(String(t.schema), String(t.name))}${columns.length > 0 ? ` (${columns.join(", ")})` : ""}${str(t.filter) ? ` WHERE (${t.filter})` : ""}`;
510
+ }),
511
+ ];
512
+ const clauses: string[] = [];
513
+ if (r.alltables) clauses.push("FOR ALL TABLES");
514
+ else if (members.length > 0 || schemas.length > 0) {
515
+ const parts: string[] = [];
516
+ if (members.length > 0) parts.push(`TABLE ${members.join(", ")}`);
517
+ if (schemas.length > 0) parts.push(`TABLES IN SCHEMA ${schemas.map((x) => quoteIdent(String(x.name))).join(", ")}`);
518
+ clauses.push(`FOR ${parts.join(", ")}`);
519
+ }
520
+ const publish = [r.pubinsert && "insert", r.pubupdate && "update", r.pubdelete && "delete", r.pubtruncate && "truncate"].filter(Boolean);
521
+ const options: string[] = [];
522
+ if (publish.length !== 4) options.push(`publish = ${literal(publish.join(", "))}`);
523
+ if (d.pubviaroot === true) options.push("publish_via_partition_root = true");
524
+ if (d.pubgencols !== undefined && d.pubgencols !== "n") options.push("publish_generated_columns = stored");
525
+ if (options.length > 0) clauses.push(`WITH (${options.join(", ")})`);
526
+ out.push({
527
+ type: POSTGRES_ENTITY_TYPES.publication,
528
+ name: String(r.name),
529
+ oid: String(r.oid),
530
+ ...withComment(r),
531
+ statement: [`CREATE PUBLICATION ${name}${clauses.length > 0 ? ` ${clauses.join(" ")}` : ""}`, ...commentOn(`PUBLICATION ${name}`, r.comment)].join(";\n"),
532
+ });
533
+ }
534
+ return out;
535
+ }
@@ -53,7 +53,7 @@ import { MIGRATION_TRAILER_KEY } from "../migrate/names";
53
53
  import { isProviderOwned, providerData } from "../providers";
54
54
  import { FOREIGN_TABLES } from "../../core/foreign-tables";
55
55
  import type { PostgresProvider } from "../providers/types";
56
- import { readCollations, readRoutineKinds, readTypeKinds } from "./catalog-kinds";
56
+ import { readCollations, readDatabaseKinds, readRoutineKinds, readTypeKinds } from "./catalog-kinds";
57
57
 
58
58
  /** The server's own schemas, never part of a declared schema. */
59
59
  export const SYSTEM_SCHEMAS = ["pg_catalog", "information_schema", "pg_toast"];
@@ -83,6 +83,8 @@ export interface LivePgObject {
83
83
  dependents?: string[];
84
84
  /** Why no declaration can hold this object; it is read so import can say it left it out. */
85
85
  unsupported?: string;
86
+ /** What was left out of an object that is otherwise read (an option that holds a credential), for import to say. */
87
+ warnings?: string[];
86
88
  }
87
89
 
88
90
  /** What {@link readLiveSchema} reads. */
@@ -629,6 +631,9 @@ export async function readLiveSchema(client: PostgresClient, scope: SchemaScope
629
631
  // Aggregates, casts and event triggers, after the functions they call (#3911).
630
632
  out.push(...(await readRoutineKinds(client, scope)));
631
633
 
634
+ // Extended statistics, foreign data wrappers and servers, and publications (#3911).
635
+ out.push(...(await readDatabaseKinds(client, scope)));
636
+
632
637
  // Triggers: the ones written by hand, not a foreign key's or a partition's clone of its parent's.
633
638
  {
634
639
  const rows = await client.query<Row>(
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Extended statistics, foreign data wrappers, foreign servers and
3
+ * publications against a live server (#3911): applied as declared, they plan
4
+ * with no change and read back with no drift; a definition changed in the
5
+ * declaration is altered in place (SQLPG303) or dropped and created
6
+ * (SQLPG300); an import writes each back as the declaration it came from,
7
+ * which plans with no change.
8
+ *
9
+ * The server: a throwaway one at the pin, skipped cleanly when Docker is not
10
+ * available. A foreign data wrapper takes a superuser, which the throwaway
11
+ * server's user is.
12
+ */
13
+
14
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
18
+ import { dockerAvailable, startTestPostgres, type TestPostgres } from "../testing/server";
19
+ import { connectPostgres, type PostgresClient, type PostgresEndpoint } from "./client";
20
+ import { readLiveSchema } from "./catalog";
21
+ import { planPgAgainstServer } from "../plan/commands";
22
+ import { observeResourcesDeep, liveProps } from "../plan/deep";
23
+ import { postgresApply } from "../../op/activities/postgres-apply";
24
+ import { objectsToIR } from "../import/ir";
25
+ import { PostgresGenerator, postgresTemplates } from "../import/generator";
26
+ import { declaredObjects } from "../apply/apply";
27
+
28
+ const SCHEMA = "chant_e2e_3911b";
29
+ const enabled = await dockerAvailable();
30
+ const dir = mkdtempSync(join(tmpdir(), "chant-pg-database-kinds-"));
31
+ let server: TestPostgres | undefined;
32
+ let endpoint: PostgresEndpoint;
33
+ let admin: PostgresClient;
34
+
35
+ beforeAll(async () => {
36
+ if (!enabled) return;
37
+ server = await startTestPostgres();
38
+ endpoint = server.endpoint();
39
+ admin = await connectPostgres(endpoint);
40
+ }, 600_000);
41
+
42
+ afterAll(async () => {
43
+ await admin?.end();
44
+ await server?.stop();
45
+ rmSync(dir, { recursive: true, force: true });
46
+ });
47
+
48
+ interface Obj {
49
+ export: string;
50
+ type: string;
51
+ ddl: string;
52
+ dependsOn?: string[];
53
+ }
54
+
55
+ const MARKER = { stack: "e2e3911b", env: "test" };
56
+ const profile = () => ({
57
+ config: { ownership: MARKER, sql: { profiles: { e2e: { url: endpoint.url, password: { env: "PG_E2E_PASSWORD" }, schemas: [SCHEMA] } } } },
58
+ env: { PG_E2E_PASSWORD: endpoint.password ?? "" },
59
+ });
60
+
61
+ function writeBuild(file: string, objects: Obj[]): string {
62
+ const path = join(dir, file);
63
+ writeFileSync(path, JSON.stringify({ dialect: "postgres", postgresMajor: 18, objects: objects.map((o) => ({ dependsOn: [], ...o })) }));
64
+ return path;
65
+ }
66
+
67
+ const apply = (path: string) => postgresApply({ buildPath: path, environment: "e2e" }, undefined, { ...profile(), log: () => undefined });
68
+ const plan = (path: string) => planPgAgainstServer("e2e", path, profile());
69
+
70
+ const S = SCHEMA;
71
+ interface Version {
72
+ /** The statistics' kinds. */
73
+ kinds: string;
74
+ /** The wrapper's options. */
75
+ fdwOptions: string;
76
+ /** The server's version and options. */
77
+ server: string;
78
+ /** The publication's tables and options. */
79
+ publication: string;
80
+ }
81
+
82
+ const objects = (v: Version): Obj[] => [
83
+ { export: "app", type: "Postgres::Schema", ddl: `CREATE SCHEMA ${S}` },
84
+ { export: "orders", type: "Postgres::Table", dependsOn: ["app"], ddl: `CREATE TABLE ${S}.orders (id int PRIMARY KEY, user_id int, status text)` },
85
+ { export: "events", type: "Postgres::Table", dependsOn: ["app"], ddl: `CREATE TABLE ${S}.events (id int PRIMARY KEY)` },
86
+ { export: "ordersStats", type: "Postgres::Statistics", dependsOn: ["orders"], ddl: `CREATE STATISTICS ${S}.orders_stats ${v.kinds} ON user_id, status FROM ${S}.orders;\nCOMMENT ON STATISTICS ${S}.orders_stats IS 'Correlated'` },
87
+ { export: "appFdw", type: "Postgres::ForeignDataWrapper", ddl: `CREATE FOREIGN DATA WRAPPER app_fdw_3911 ${v.fdwOptions};\nCOMMENT ON FOREIGN DATA WRAPPER app_fdw_3911 IS 'Remote'` },
88
+ { export: "replica", type: "Postgres::ForeignServer", dependsOn: ["appFdw"], ddl: `CREATE SERVER replica_3911 ${v.server} FOREIGN DATA WRAPPER app_fdw_3911 OPTIONS (host 'db.internal');\nCOMMENT ON SERVER replica_3911 IS 'The replica'` },
89
+ { export: "pub", type: "Postgres::Publication", dependsOn: ["orders", "events"], ddl: `CREATE PUBLICATION pub_3911 ${v.publication};\nCOMMENT ON PUBLICATION pub_3911 IS 'Order changes'` },
90
+ ];
91
+
92
+ const V1: Version = {
93
+ kinds: "(dependencies)",
94
+ fdwOptions: "OPTIONS (debug 'true')",
95
+ server: "TYPE 'pg' VERSION '16'",
96
+ publication: `FOR TABLE ${S}.orders WITH (publish = 'insert, update')`,
97
+ };
98
+ // Each kind changed: the statistics' kinds (drop and create), the wrapper's options, the server's version and the publication's tables and options (in place).
99
+ const V2: Version = {
100
+ kinds: "(ndistinct)",
101
+ fdwOptions: "OPTIONS (debug 'false', trace 'on')",
102
+ server: "TYPE 'pg' VERSION '17'",
103
+ publication: `FOR TABLE ${S}.events WITH (publish = 'insert')`,
104
+ };
105
+
106
+ describe.skipIf(!enabled)("statistics, foreign servers and publications on a live server", () => {
107
+ test("apply, plan, drift, change and import", async () => {
108
+ const v1 = writeBuild("v1.json", objects(V1));
109
+ const first = await apply(v1);
110
+ expect(first.failed).toEqual([]);
111
+ expect(first.applied.map((a) => [a.name, a.action])).toEqual([
112
+ [S, "created"],
113
+ [`${S}.orders`, "created"],
114
+ [`${S}.events`, "created"],
115
+ [`${S}.orders_stats`, "created"],
116
+ ["app_fdw_3911", "created"],
117
+ ["replica_3911", "created"],
118
+ ["pub_3911", "created"],
119
+ ]);
120
+
121
+ // As declared: no change, and no drift.
122
+ expect((await plan(v1)).changes).toEqual([]);
123
+ const declared = declaredObjects(JSON.stringify({ dialect: "postgres", objects: objects(V1).map((o) => ({ dependsOn: [], ...o })) }));
124
+ const entities = new Map(declared.map((o) => [o.exportName, { entityType: o.type, props: o.props }]));
125
+ const deep = await observeResourcesDeep({ environment: "e2e", entityNames: [...entities.keys()], entities, ...profile() });
126
+ expect(deep.unobserved ?? {}).toEqual({});
127
+
128
+ // Changed by hand: drift, in the plan.
129
+ await admin.query(`ALTER SERVER replica_3911 OPTIONS (SET host 'elsewhere')`);
130
+ expect((await plan(v1)).changes.map((c) => [c.object, c.field, c.rule])).toEqual([["replica (replica_3911)", "options", "SQLPG303"]]);
131
+ await admin.query(`ALTER SERVER replica_3911 OPTIONS (SET host 'db.internal')`);
132
+
133
+ // Changed in the declaration: the statistics are dropped and created; the rest altered in place.
134
+ const v2 = writeBuild("v2.json", objects(V2));
135
+ const planned = await plan(v2);
136
+ expect(planned.changes.map((c) => [c.object, c.rule])).toEqual([
137
+ [`ordersStats (${S}.orders_stats)`, "SQLPG300"],
138
+ ["appFdw (app_fdw_3911)", "SQLPG303"],
139
+ ["replica (replica_3911)", "SQLPG303"],
140
+ ["pub (pub_3911)", "SQLPG303"],
141
+ ["pub (pub_3911)", "SQLPG303"],
142
+ ["pub (pub_3911)", "SQLPG303"],
143
+ ]);
144
+ expect(planned.refused).toEqual([]);
145
+ const second = await apply(v2);
146
+ expect(second.failed).toEqual([]);
147
+ expect((await plan(v2)).changes).toEqual([]);
148
+ expect((await admin.query<{ n: number }>(`SELECT count(*)::int AS n FROM pg_publication_rel r JOIN pg_publication p ON p.oid = r.prpubid WHERE p.pubname = 'pub_3911'`))[0]!.n).toBe(1);
149
+
150
+ // Import: the server's own printing, written back as declarations that parse and plan with no change.
151
+ const live = await readLiveSchema(admin, { schemas: [S] });
152
+ for (const o of live) liveProps(o);
153
+ const ir = objectsToIR(live.map((o) => ({ type: o.type, ...(o.schema ? { schema: o.schema } : {}), name: o.name, ddl: o.statement })));
154
+ const [file] = new PostgresGenerator().generate(ir);
155
+ expect(file!.content).toContain("export const ordersStats = statistics`");
156
+ expect(file!.content).toContain("export const appFdw3911 = foreignDataWrapper`");
157
+ expect(file!.content).toContain("FOREIGN DATA WRAPPER ${appFdw3911}");
158
+ expect(file!.content).toContain("export const pub3911 = publication`");
159
+ const items = ir.resources.map((r) => ({ exportName: r.logicalId, type: r.type, ...(r.properties.schema ? { schema: String(r.properties.schema) } : {}), name: String(r.properties.name), ddl: String(r.properties.ddl) }));
160
+ const { order } = postgresTemplates(items);
161
+ const imported = writeBuild("imported.json", order.map((it) => ({ export: it.exportName, type: it.type, ddl: it.ddl })));
162
+ expect((await plan(imported)).changes).toEqual([]);
163
+ }, 600_000);
164
+ });
@@ -34,6 +34,14 @@ interface Bound {
34
34
  byKey: Map<string, LivePgObject>;
35
35
  }
36
36
 
37
+ /** The entity types that belong to the database, not a schema. */
38
+ const UNPLACED: ReadonlySet<string> = new Set([
39
+ POSTGRES_ENTITY_TYPES.eventTrigger,
40
+ POSTGRES_ENTITY_TYPES.foreignDataWrapper,
41
+ POSTGRES_ENTITY_TYPES.foreignServer,
42
+ POSTGRES_ENTITY_TYPES.publication,
43
+ ]);
44
+
37
45
  /**
38
46
  * The schema and name a declared entity is created as; a bare name is in the
39
47
  * default schema. A routine's parameter types and a trigger's table tell it
@@ -41,7 +49,7 @@ interface Bound {
41
49
  */
42
50
  export function declaredAddress(entity: { type: string; props: Record<string, unknown> }, defaultSchema: string): { schema?: string; name: string; signature?: string } {
43
51
  const name = String(entity.props.name ?? "");
44
- if (entity.type === POSTGRES_ENTITY_TYPES.schema || entity.type === POSTGRES_ENTITY_TYPES.extension || entity.type === POSTGRES_ENTITY_TYPES.role || entity.type === POSTGRES_ENTITY_TYPES.eventTrigger) return { name };
52
+ if (entity.type === POSTGRES_ENTITY_TYPES.schema || entity.type === POSTGRES_ENTITY_TYPES.extension || entity.type === POSTGRES_ENTITY_TYPES.role || UNPLACED.has(entity.type)) return { name };
45
53
  // A cast is named by its two types, as the catalog prints them.
46
54
  if (entity.type === POSTGRES_ENTITY_TYPES.cast) return { name: canonicalPgObject(entity.type, entity.props, defaultSchema).name };
47
55
  const schema = typeof entity.props.schema === "string" ? entity.props.schema : identValue(defaultSchema);
@@ -49,6 +57,7 @@ export function declaredAddress(entity: { type: string; props: Record<string, un
49
57
  entity.type === POSTGRES_ENTITY_TYPES.function ||
50
58
  entity.type === POSTGRES_ENTITY_TYPES.procedure ||
51
59
  entity.type === POSTGRES_ENTITY_TYPES.aggregate ||
60
+ entity.type === POSTGRES_ENTITY_TYPES.operator ||
52
61
  entity.type === POSTGRES_ENTITY_TYPES.trigger ||
53
62
  entity.type === POSTGRES_ENTITY_TYPES.policy
54
63
  ) {
@@ -79,7 +88,7 @@ export function scopeFor(target: PostgresTarget, entities: ReadonlyArray<{ type:
79
88
  if (entities.length === 0) return undefined;
80
89
  const out = new Set<string>([target.defaultSchema]);
81
90
  // The schema alone: the rest of an address (a routine's parameter types) needs props a caller may not have.
82
- const unplaced = new Set<string>([POSTGRES_ENTITY_TYPES.extension, POSTGRES_ENTITY_TYPES.role, POSTGRES_ENTITY_TYPES.cast, POSTGRES_ENTITY_TYPES.eventTrigger]);
91
+ const unplaced = new Set<string>([POSTGRES_ENTITY_TYPES.extension, POSTGRES_ENTITY_TYPES.role, POSTGRES_ENTITY_TYPES.cast, ...UNPLACED]);
83
92
  for (const e of entities) {
84
93
  if (e.type === POSTGRES_ENTITY_TYPES.schema) out.add(String(e.props.name ?? ""));
85
94
  else if (!unplaced.has(e.type)) out.add(typeof e.props.schema === "string" ? e.props.schema : identValue(target.defaultSchema));
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Import against a real Postgres server never leaves an object out without a
3
3
  * word (#3911): a database holding a rewrite rule, an encoding conversion, an
4
- * access method and a shell type beside a table fails the import, which
5
- * names all four; with `skipUndeclarable` the rest is imported and the four
6
- * are warnings. A composite type, an aggregate, a collation, a cast and an
7
- * event trigger are declared kinds, so they are imported, not refused. What
4
+ * access method, a shell type, a user mapping and a subscription (the last
5
+ * two carry credentials) beside a table fails the import, which names all
6
+ * six; with `skipUndeclarable` the rest is imported and the six are warnings.
7
+ * A composite type, an aggregate, a collation, a cast, an event trigger, a
8
+ * foreign data wrapper, a foreign server and a publication are declared
9
+ * kinds, so they are imported, not refused. What
8
10
  * an extension brings (hstore's casts and operators) is the extension's, and
9
11
  * is not refused.
10
12
  *
@@ -36,6 +38,11 @@ CREATE RULE users_notify AS ON INSERT TO app.users DO ALSO NOTIFY users;
36
38
  CREATE CONVERSION app.latin1_utf8 FOR 'LATIN1' TO 'UTF8' FROM iso8859_1_to_utf8;
37
39
  CREATE ACCESS METHOD heap2 TYPE TABLE HANDLER heap_tableam_handler;
38
40
  CREATE TYPE app.later;
41
+ CREATE FOREIGN DATA WRAPPER app_fdw;
42
+ CREATE SERVER app_remote FOREIGN DATA WRAPPER app_fdw OPTIONS (host 'db.internal');
43
+ CREATE USER MAPPING FOR PUBLIC SERVER app_remote OPTIONS (user 'reader');
44
+ CREATE PUBLICATION app_pub FOR TABLE app.users;
45
+ CREATE SUBSCRIPTION app_sub CONNECTION 'host=127.0.0.1 dbname=elsewhere' PUBLICATION elsewhere WITH (connect = false);
39
46
  `;
40
47
 
41
48
  beforeAll(async () => {
@@ -58,7 +65,7 @@ const profile = () => ({
58
65
  env: { PG_UD_PASSWORD: server!.endpoint("source").password },
59
66
  });
60
67
 
61
- const NAMED = ["rule app.users_notify ON users", "conversion app.latin1_utf8", "access method heap2", "base type app.later"];
68
+ const NAMED = ["rule app.users_notify ON users", "conversion app.latin1_utf8", "access method heap2", "base type app.later", "user mapping for public on server app_remote", "subscription app_sub"];
62
69
 
63
70
  describe.skipIf(!enabled)("importing a database with objects chant cannot declare", () => {
64
71
  test("fails, naming every one of them", async () => {
@@ -75,7 +82,7 @@ describe.skipIf(!enabled)("importing a database with objects chant cannot declar
75
82
  test("with skipUndeclarable, imports the rest and lists them as warnings", async () => {
76
83
  const ir = await exportResources({ environment: "src", skipUndeclarable: true, ...profile() });
77
84
  expect(ir.resources.map((r) => r.properties.name)).toEqual(
78
- expect.arrayContaining(["users", "hstore", "add2", "pair_text", "on_ddl", "pair", "total", "ci", "(app.pair AS text)", "audit_ddl"]),
85
+ expect.arrayContaining(["users", "hstore", "add2", "pair_text", "on_ddl", "pair", "total", "ci", "(app.pair AS text)", "audit_ddl", "app_fdw", "app_remote", "app_pub"]),
79
86
  );
80
87
  const left = (ir.warnings ?? []).filter((w) => w.includes("is left out (--skip-undeclarable)"));
81
88
  expect(left.map((w) => w.split(" is left out")[0]).sort()).toEqual([...NAMED].sort());
@@ -169,6 +169,8 @@ export interface VerifyResult {
169
169
  mismatched: number;
170
170
  /** Rows whose new column is NULL. */
171
171
  nulls: number;
172
+ /** A rename with a type change: rows whose old column differs from `reverse` over the new one. */
173
+ unreversed?: number;
172
174
  /** The sum of a 64-bit hash of every new value, and of every value the expression gives. */
173
175
  checksum: string;
174
176
  expected: string;
@@ -201,6 +203,7 @@ export async function verifyMigration(run: MigrationRun): Promise<VerifyResult>
201
203
  exec(
202
204
  `SELECT count(*)::text AS rows, count(*) FILTER (WHERE ${nw} IS DISTINCT FROM (${o.expression}))::text AS mismatched,
203
205
  count(*) FILTER (WHERE ${nw} IS NULL)::text AS nulls,
206
+ ${o.reverse !== undefined ? `count(*) FILTER (WHERE (${o.reverse}) IS DISTINCT FROM ${col(n.source)})::text` : "'0'"} AS unreversed,
204
207
  COALESCE(sum(pg_catalog.hashtextextended(${nw}::text, 0)), 0)::text AS checksum,
205
208
  COALESCE(sum(pg_catalog.hashtextextended((${o.expression})::text, 0)), 0)::text AS expected
206
209
  FROM ${n.qualifiedTable}`,
@@ -211,6 +214,14 @@ export async function verifyMigration(run: MigrationRun): Promise<VerifyResult>
211
214
  if (r.mismatched > 0 || r.checksum !== r.expected) {
212
215
  throw new MigrationVerificationError(n.key, { state: o.state, ...r }, `${r.mismatched} of ${r.rows} row(s) have ${n.newColumn} different from ${o.expression} (checksums ${r.checksum} and ${r.expected})`);
213
216
  }
217
+ const unreversed = Number(row?.unreversed ?? 0);
218
+ if (o.reverse !== undefined && unreversed > 0) {
219
+ throw new MigrationVerificationError(
220
+ n.key,
221
+ { state: o.state, ...r, unreversed },
222
+ `${unreversed} of ${r.rows} row(s) have ${n.source} different from ${o.reverse}, which must give the old value back from the new one (it would write ${n.source} wrongly for a writer of ${n.column})`,
223
+ );
224
+ }
214
225
  if (requiresNotNull(o.column, o.carried) && r.nulls > 0) {
215
226
  throw new MigrationVerificationError(n.key, { state: o.state, ...r }, `${r.nulls} of ${r.rows} row(s) have ${n.newColumn} NULL, and ${n.column} is declared NOT NULL or is in the primary key`);
216
227
  }
@@ -222,12 +233,13 @@ export async function verifyMigration(run: MigrationRun): Promise<VerifyResult>
222
233
  o.carried.length > 0 || o.views.length > 0
223
234
  ? `; ${o.carried.map((c) => `${c.kind} ${c.name}${c.target !== c.name ? ` (as ${c.target})` : ""}`).join(", ") || "no index or constraint"} carried over${o.views.length > 0 ? `, view(s) ${o.views.map((v) => v.name).join(", ")} made again at the switch` : ""}`
224
235
  : "";
225
- const summary = `${n.key}: ${r.rows} row(s), ${n.newColumn} equal to ${o.expression} in every one (checksum ${r.checksum})${carried}`;
236
+ const summary = `${n.key}: ${r.rows} row(s), ${n.newColumn} equal to ${o.expression} in every one (checksum ${r.checksum})${o.reverse !== undefined ? `, ${n.source} equal to ${o.reverse} in every one` : ""}${carried}`;
226
237
  run.log(`-- ${summary}`);
227
238
  return {
228
239
  state: o.state,
229
240
  planDigest: computePlanDigest("postgres-migration", { plan: migrationPlanSubject(o), table: o.oid, column: o.newColumn.attnum, verified: { mismatched: 0 } }),
230
241
  ...r,
242
+ ...(o.reverse !== undefined ? { unreversed } : {}),
231
243
  summary,
232
244
  };
233
245
  }