@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.
- package/dist/change-set.d.ts +1 -1
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +2 -1
- package/dist/clickhouse/apply/statements.d.ts +9 -0
- package/dist/clickhouse/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/apply/statements.js +24 -0
- package/dist/clickhouse/import/live-export.d.ts +7 -3
- package/dist/clickhouse/import/live-export.d.ts.map +1 -1
- package/dist/clickhouse/import/live-export.js +21 -5
- package/dist/clickhouse/live/named-collections.d.ts +27 -0
- package/dist/clickhouse/live/named-collections.d.ts.map +1 -0
- package/dist/clickhouse/live/named-collections.js +53 -0
- package/dist/clickhouse/live/undeclarable.d.ts +15 -3
- package/dist/clickhouse/live/undeclarable.d.ts.map +1 -1
- package/dist/clickhouse/live/undeclarable.js +18 -4
- package/dist/clickhouse/named-collections.d.ts +83 -0
- package/dist/clickhouse/named-collections.d.ts.map +1 -0
- package/dist/clickhouse/named-collections.js +203 -0
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/parser.js +1 -1
- package/dist/clickhouse/plan/commands.d.ts +2 -1
- package/dist/clickhouse/plan/commands.d.ts.map +1 -1
- package/dist/clickhouse/plan/commands.js +17 -2
- package/dist/clickhouse/plan/diff.d.ts +15 -0
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.js +43 -1
- package/dist/clickhouse/plan/disruption.d.ts.map +1 -1
- package/dist/clickhouse/plan/disruption.js +5 -0
- package/dist/clickhouse/plan/rebuild-handoff.d.ts.map +1 -1
- package/dist/clickhouse/plan/rebuild-handoff.js +4 -0
- package/dist/clickhouse/plan/report.d.ts.map +1 -1
- package/dist/clickhouse/plan/report.js +6 -1
- package/dist/clickhouse/plan/rules.d.ts +5 -1
- package/dist/clickhouse/plan/rules.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.js +7 -3
- package/dist/clickhouse/plan/stream.d.ts +40 -0
- package/dist/clickhouse/plan/stream.d.ts.map +1 -0
- package/dist/clickhouse/plan/stream.js +107 -0
- package/dist/clickhouse/rebuild/observe.d.ts +3 -1
- package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/observe.js +14 -1
- package/dist/clickhouse/review-catalog.d.ts +213 -0
- package/dist/clickhouse/review-catalog.d.ts.map +1 -0
- package/dist/clickhouse/review-catalog.js +330 -0
- package/dist/codegen/classifier-page.d.ts.map +1 -1
- package/dist/codegen/classifier-page.js +21 -4
- package/dist/codegen/clickhouse-module.d.ts.map +1 -1
- package/dist/codegen/clickhouse-module.js +3 -0
- package/dist/codegen/coverage-page.d.ts.map +1 -1
- package/dist/codegen/coverage-page.js +5 -3
- package/dist/codegen/docs.js +1 -1
- package/dist/codegen/postgres-classifier-page.js +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/codegen/registry.js +5 -0
- package/dist/composites/catalog.d.ts.map +1 -1
- package/dist/composites/catalog.js +100 -0
- package/dist/composites/clickhouse.d.ts +2 -0
- package/dist/composites/clickhouse.d.ts.map +1 -1
- package/dist/composites/clickhouse.js +2 -0
- package/dist/composites/otel-tables.d.ts +100 -0
- package/dist/composites/otel-tables.d.ts.map +1 -0
- package/dist/composites/otel-tables.js +282 -0
- package/dist/composites/stream-ingest.d.ts +63 -0
- package/dist/composites/stream-ingest.d.ts.map +1 -0
- package/dist/composites/stream-ingest.js +39 -0
- package/dist/coverage/clickhouse.json +1 -1
- package/dist/coverage/postgres.json +7 -7
- package/dist/coverage.d.ts +16 -6
- package/dist/coverage.d.ts.map +1 -1
- package/dist/coverage.js +10 -5
- package/dist/drizzle-command.d.ts +17 -0
- package/dist/drizzle-command.d.ts.map +1 -0
- package/dist/drizzle-command.js +91 -0
- package/dist/drizzle.d.ts +70 -0
- package/dist/drizzle.d.ts.map +1 -0
- package/dist/drizzle.js +684 -0
- package/dist/files/clickhouse.d.ts.map +1 -1
- package/dist/files/clickhouse.js +8 -1
- package/dist/files/common.d.ts +13 -0
- package/dist/files/common.d.ts.map +1 -1
- package/dist/files/common.js +5 -0
- package/dist/files/postgres.js +12 -2
- package/dist/generated/clickhouse.d.ts +2 -0
- package/dist/generated/clickhouse.d.ts.map +1 -1
- package/dist/generated/clickhouse.js +2 -0
- package/dist/import-parser.d.ts +21 -1
- package/dist/import-parser.d.ts.map +1 -1
- package/dist/import-parser.js +79 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/integrity.json +6 -5
- package/dist/lint/post-synth/audit-entries.d.ts.map +1 -1
- package/dist/lint/post-synth/audit-entries.js +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/index.js +2 -0
- package/dist/lint/post-synth/sqlch128.d.ts +18 -0
- package/dist/lint/post-synth/sqlch128.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch128.js +222 -0
- package/dist/lint/rules/postgres-templates.js +2 -2
- package/dist/lsp/postgres-template.d.ts.map +1 -1
- package/dist/lsp/postgres-template.js +11 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/postgres.js +1 -1
- package/dist/meta.json +25 -0
- package/dist/migrate/from-postgres/detect.d.ts +4 -0
- package/dist/migrate/from-postgres/detect.d.ts.map +1 -0
- package/dist/migrate/from-postgres/detect.js +7 -0
- package/dist/migrate/from-postgres/emit.d.ts +26 -0
- package/dist/migrate/from-postgres/emit.d.ts.map +1 -0
- package/dist/migrate/from-postgres/emit.js +86 -0
- package/dist/migrate/from-postgres/index.d.ts +51 -0
- package/dist/migrate/from-postgres/index.d.ts.map +1 -0
- package/dist/migrate/from-postgres/index.js +115 -0
- package/dist/migrate/from-postgres/translate.d.ts +73 -0
- package/dist/migrate/from-postgres/translate.d.ts.map +1 -0
- package/dist/migrate/from-postgres/translate.js +261 -0
- package/dist/migrate/from-postgres/types.d.ts +51 -0
- package/dist/migrate/from-postgres/types.d.ts.map +1 -0
- package/dist/migrate/from-postgres/types.js +211 -0
- package/dist/migration-replay.d.ts +73 -0
- package/dist/migration-replay.d.ts.map +1 -0
- package/dist/migration-replay.js +294 -0
- package/dist/okf/index.md +6 -0
- package/dist/okf/rules/SQLCH128.md +11 -0
- package/dist/okf/types/PostgresForeignDataWrapper.md +9 -0
- package/dist/okf/types/PostgresForeignServer.md +9 -0
- package/dist/okf/types/PostgresOperator.md +9 -0
- package/dist/okf/types/PostgresPublication.md +9 -0
- package/dist/okf/types/PostgresStatistics.md +9 -0
- package/dist/op/activities/postgres-migration.d.ts.map +1 -1
- package/dist/op/activities/postgres-migration.js +4 -0
- package/dist/op/activity-contracts.d.ts +20 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/activity-contracts.js +2 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +36 -2
- package/dist/postgres/apply/apply.js +8 -1
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/apply/statements.js +87 -2
- package/dist/postgres/constraint-names.d.ts +30 -0
- package/dist/postgres/constraint-names.d.ts.map +1 -0
- package/dist/postgres/constraint-names.js +72 -0
- package/dist/postgres/entities.d.ts +110 -1
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entities.js +146 -0
- package/dist/postgres/entity-types.d.ts +5 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/entity-types.js +5 -0
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/generator.js +50 -2
- package/dist/postgres/import/live-export.d.ts.map +1 -1
- package/dist/postgres/import/live-export.js +2 -0
- package/dist/postgres/live/catalog-kinds.d.ts +14 -0
- package/dist/postgres/live/catalog-kinds.d.ts.map +1 -1
- package/dist/postgres/live/catalog-kinds.js +206 -0
- package/dist/postgres/live/catalog.d.ts +2 -0
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/catalog.js +3 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.js +10 -2
- package/dist/postgres/migrate/backfill.d.ts +2 -0
- package/dist/postgres/migrate/backfill.d.ts.map +1 -1
- package/dist/postgres/migrate/backfill.js +7 -1
- package/dist/postgres/migrate/carry.d.ts +45 -1
- package/dist/postgres/migrate/carry.d.ts.map +1 -1
- package/dist/postgres/migrate/carry.js +72 -8
- package/dist/postgres/migrate/observe.d.ts +28 -4
- package/dist/postgres/migrate/observe.d.ts.map +1 -1
- package/dist/postgres/migrate/observe.js +41 -12
- package/dist/postgres/migrate/op.d.ts +26 -1
- package/dist/postgres/migrate/op.d.ts.map +1 -1
- package/dist/postgres/migrate/op.js +3 -1
- package/dist/postgres/migrate/steps.d.ts +35 -2
- package/dist/postgres/migrate/steps.d.ts.map +1 -1
- package/dist/postgres/migrate/steps.js +158 -12
- package/dist/postgres/parser.d.ts +72 -1
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/parser.js +194 -2
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/deep.js +29 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/diff.js +39 -1
- package/dist/postgres/plan/normalize.d.ts +17 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.js +109 -1
- package/dist/postgres/plan/rules.d.ts +1 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/rules.js +3 -2
- package/dist/postgres/plan/schema.js +1 -1
- package/dist/postgres/secret-options.d.ts +10 -0
- package/dist/postgres/secret-options.d.ts.map +1 -0
- package/dist/postgres/secret-options.js +9 -0
- package/dist/postgres/testing/roundtrip-schema.d.ts +3 -2
- package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
- package/dist/postgres/testing/roundtrip-schema.js +13 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +1 -1
- package/dist/rules/audit-entries.ts +1 -0
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sqlch128.ts +230 -0
- package/dist/serializer.d.ts +5 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +13 -2
- package/dist/sql-types-command.d.ts +13 -0
- package/dist/sql-types-command.d.ts.map +1 -1
- package/dist/sql-types-command.js +31 -16
- package/dist/sql-types.d.ts +22 -0
- package/dist/sql-types.d.ts.map +1 -1
- package/dist/sql-types.js +10 -6
- package/dist/sqlserver/lexer.d.ts +37 -0
- package/dist/sqlserver/lexer.d.ts.map +1 -0
- package/dist/sqlserver/lexer.js +254 -0
- package/dist/sqlserver/parse.d.ts +185 -0
- package/dist/sqlserver/parse.d.ts.map +1 -0
- package/dist/sqlserver/parse.js +767 -0
- package/dist/sqlserver/translate.d.ts +34 -0
- package/dist/sqlserver/translate.d.ts.map +1 -0
- package/dist/sqlserver/translate.js +495 -0
- package/dist/sqlserver/types.d.ts +44 -0
- package/dist/sqlserver/types.d.ts.map +1 -0
- package/dist/sqlserver/types.js +133 -0
- package/dist/sqlserver/validate.d.ts +31 -0
- package/dist/sqlserver/validate.d.ts.map +1 -0
- package/dist/sqlserver/validate.js +67 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +5 -0
- package/package.json +2 -2
- package/src/change-set.ts +2 -1
- package/src/clickhouse/apply/statements.ts +25 -0
- package/src/clickhouse/import/live-export.ts +20 -5
- package/src/clickhouse/live/named-collections.e2e.test.ts +88 -0
- package/src/clickhouse/live/named-collections.ts +61 -0
- package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -3
- package/src/clickhouse/live/undeclarable.ts +20 -4
- package/src/clickhouse/named-collections.test.ts +187 -0
- package/src/clickhouse/named-collections.ts +224 -0
- package/src/clickhouse/parser.ts +2 -1
- package/src/clickhouse/plan/commands.ts +17 -2
- package/src/clickhouse/plan/diff.ts +57 -1
- package/src/clickhouse/plan/disruption.ts +5 -0
- package/src/clickhouse/plan/rebuild-handoff.ts +3 -0
- package/src/clickhouse/plan/report.ts +6 -1
- package/src/clickhouse/plan/rules.ts +14 -4
- package/src/clickhouse/plan/stream-s3queue.e2e.test.ts +148 -0
- package/src/clickhouse/plan/stream.test.ts +237 -0
- package/src/clickhouse/plan/stream.ts +111 -0
- package/src/clickhouse/rebuild/observe.ts +15 -1
- package/src/clickhouse/review-catalog.test.ts +181 -0
- package/src/clickhouse/review-catalog.ts +489 -0
- package/src/codegen/classifier-page.ts +21 -4
- package/src/codegen/clickhouse-module.ts +3 -0
- package/src/codegen/coverage-page.ts +5 -3
- package/src/codegen/docs.ts +1 -1
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +5 -0
- package/src/composites/catalog.test.ts +6 -1
- package/src/composites/catalog.ts +100 -0
- package/src/composites/clickhouse.ts +2 -0
- package/src/composites/composites.test.ts +1 -1
- package/src/composites/otel-tables.test.ts +118 -0
- package/src/composites/otel-tables.ts +339 -0
- package/src/composites/stream-ingest.test.ts +114 -0
- package/src/composites/stream-ingest.ts +82 -0
- package/src/core/undeclarable.test.ts +6 -5
- package/src/coverage/clickhouse.json +1 -1
- package/src/coverage/postgres.json +7 -7
- package/src/coverage.test.ts +27 -2
- package/src/coverage.ts +18 -7
- package/src/drizzle-command.ts +94 -0
- package/src/drizzle-kit.e2e.test.ts +89 -0
- package/src/drizzle-types.e2e.test.ts +67 -0
- package/src/drizzle.test.ts +358 -0
- package/src/drizzle.ts +737 -0
- package/src/files/clickhouse.ts +8 -1
- package/src/files/common.ts +14 -1
- package/src/files/postgres.ts +12 -2
- package/src/generated/clickhouse.ts +3 -0
- package/src/generated/lexicon-sql.json +25 -0
- package/src/import-parser.sqlserver.test.ts +92 -0
- package/src/import-parser.ts +79 -5
- package/src/index.ts +1 -1
- package/src/lint/post-synth/audit-entries.ts +1 -0
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/sqlch128.test.ts +107 -0
- package/src/lint/post-synth/sqlch128.ts +230 -0
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lsp/postgres-template.ts +11 -0
- package/src/mcp/postgres.ts +1 -1
- package/src/migrate/from-postgres/cli.test.ts +165 -0
- package/src/migrate/from-postgres/detect.ts +7 -0
- package/src/migrate/from-postgres/emit.ts +96 -0
- package/src/migrate/from-postgres/example.test.ts +205 -0
- package/src/migrate/from-postgres/index.ts +122 -0
- package/src/migrate/from-postgres/translate.test.ts +242 -0
- package/src/migrate/from-postgres/translate.ts +359 -0
- package/src/migrate/from-postgres/types.test.ts +116 -0
- package/src/migrate/from-postgres/types.ts +236 -0
- package/src/migrate/from-postgres/validate.e2e.test.ts +44 -0
- package/src/migrate/from-postgres/workspace.e2e.test.ts +114 -0
- package/src/migration-replay.e2e.test.ts +155 -0
- package/src/migration-replay.test.ts +173 -0
- package/src/migration-replay.ts +289 -0
- package/src/op/__snapshots__/watch-pipeline.test.ts.snap +2 -2
- package/src/op/activities/postgres-migration.ts +4 -0
- package/src/op/activity-contracts.ts +2 -0
- package/src/plugin.ts +37 -2
- package/src/postgres/apply/apply.ts +9 -1
- package/src/postgres/apply/statements.ts +74 -1
- package/src/postgres/constraint-names.ts +71 -0
- package/src/postgres/database-kinds.test.ts +384 -0
- package/src/postgres/entities.ts +266 -1
- package/src/postgres/entity-types.ts +5 -0
- package/src/postgres/import/generator.ts +42 -2
- package/src/postgres/import/live-export.ts +1 -0
- package/src/postgres/live/catalog-kinds.ts +223 -0
- package/src/postgres/live/catalog.ts +6 -1
- package/src/postgres/live/database-kinds.e2e.test.ts +164 -0
- package/src/postgres/live/describe-resources.ts +11 -2
- package/src/postgres/live/undeclarable.e2e.test.ts +13 -6
- package/src/postgres/migrate/backfill.ts +13 -1
- package/src/postgres/migrate/carry.ts +103 -8
- package/src/postgres/migrate/followups.e2e.test.ts +218 -0
- package/src/postgres/migrate/followups.test.ts +199 -0
- package/src/postgres/migrate/kinds.e2e.test.ts +5 -5
- package/src/postgres/migrate/observe.ts +53 -14
- package/src/postgres/migrate/op.ts +28 -1
- package/src/postgres/migrate/partition.e2e.test.ts +129 -0
- package/src/postgres/migrate/partition.test.ts +206 -0
- package/src/postgres/migrate/steps.ts +157 -12
- package/src/postgres/parser.ts +234 -2
- package/src/postgres/plan/deep.ts +28 -1
- package/src/postgres/plan/diff.ts +36 -1
- package/src/postgres/plan/normalize.ts +122 -1
- package/src/postgres/plan/rules.ts +10 -3
- package/src/postgres/plan/schema.ts +1 -1
- package/src/postgres/secret-options.ts +10 -0
- package/src/postgres/testing/roundtrip-schema.ts +13 -1
- package/src/postgres.ts +16 -0
- package/src/serializer.ts +13 -2
- package/src/skills/chant-sql-postgres-migration.md +3 -3
- package/src/skills/chant-sql-postgres.md +6 -1
- package/src/skills/chant-sql.md +2 -1
- package/src/sql-types-command.ts +38 -16
- package/src/sql-types.ts +11 -6
- package/src/sqlserver/fixture.test.ts +186 -0
- package/src/sqlserver/import-cli.test.ts +61 -0
- package/src/sqlserver/lexer.ts +260 -0
- package/src/sqlserver/parse.ts +865 -0
- package/src/sqlserver/translate.test.ts +396 -0
- package/src/sqlserver/translate.ts +558 -0
- package/src/sqlserver/types.test.ts +102 -0
- package/src/sqlserver/types.ts +154 -0
- package/src/sqlserver/validate.e2e.test.ts +32 -0
- package/src/sqlserver/validate.test.ts +49 -0
- package/src/sqlserver/validate.ts +76 -0
- package/src/validate.ts +5 -0
- package/src/workspace-kinds.test.ts +11 -0
- package/workspace-kinds.json +100 -2
|
@@ -108,10 +108,12 @@ export const pgQualified = (schema: string | undefined, name: string): string =>
|
|
|
108
108
|
export const objectIdent = (o: { kind: CanonicalKind; schema?: string; name: string; signature?: string }): string =>
|
|
109
109
|
o.kind === "cast"
|
|
110
110
|
? o.name
|
|
111
|
-
: o.kind === "schema" || o.kind === "extension" || o.kind === "role" || o.kind === "eventTrigger"
|
|
111
|
+
: o.kind === "schema" || o.kind === "extension" || o.kind === "role" || o.kind === "eventTrigger" || o.kind === "foreignDataWrapper" || o.kind === "foreignServer" || o.kind === "publication"
|
|
112
112
|
? quoteIdent(o.name)
|
|
113
113
|
: o.kind === "trigger" || o.kind === "policy"
|
|
114
114
|
? `${quoteIdent(o.name)}${o.signature ?? ""}`
|
|
115
|
+
: o.kind === "operator"
|
|
116
|
+
? `${o.schema ? `${quoteIdent(o.schema)}.` : ""}${o.name}${o.signature ?? ""}`
|
|
115
117
|
: `${pgQualified(o.schema, o.name)}${o.signature ?? ""}`;
|
|
116
118
|
|
|
117
119
|
/** The words a statement names the object's kind with. */
|
|
@@ -133,6 +135,11 @@ export const KIND_WORDS: Readonly<Record<CanonicalKind, string>> = {
|
|
|
133
135
|
collation: "COLLATION",
|
|
134
136
|
cast: "CAST",
|
|
135
137
|
eventTrigger: "EVENT TRIGGER",
|
|
138
|
+
operator: "OPERATOR",
|
|
139
|
+
statistics: "STATISTICS",
|
|
140
|
+
foreignDataWrapper: "FOREIGN DATA WRAPPER",
|
|
141
|
+
foreignServer: "SERVER",
|
|
142
|
+
publication: "PUBLICATION",
|
|
136
143
|
trigger: "TRIGGER",
|
|
137
144
|
policy: "POLICY",
|
|
138
145
|
role: "ROLE",
|
|
@@ -569,6 +576,62 @@ export function setNotNullSteps(table: string, column: string, live?: Pick<PgDif
|
|
|
569
576
|
return steps;
|
|
570
577
|
}
|
|
571
578
|
|
|
579
|
+
/** An `OPTIONS (...)` clause that takes the options `have` to `want`: ADD, SET and DROP per option; empty when they agree. */
|
|
580
|
+
function optionsClause(want: unknown, have: unknown): string {
|
|
581
|
+
const w = (want ?? {}) as Record<string, string>;
|
|
582
|
+
const h = (have ?? {}) as Record<string, string>;
|
|
583
|
+
const parts = [
|
|
584
|
+
...Object.keys(w).filter((k) => !(k in h)).map((k) => `ADD ${quoteIdent(k)} ${pgString(w[k]!)}`),
|
|
585
|
+
...Object.keys(w).filter((k) => k in h && h[k] !== w[k]).map((k) => `SET ${quoteIdent(k)} ${pgString(w[k]!)}`),
|
|
586
|
+
...Object.keys(h).filter((k) => !(k in w)).map((k) => `DROP ${quoteIdent(k)}`),
|
|
587
|
+
];
|
|
588
|
+
return parts.length > 0 ? `OPTIONS (${parts.join(", ")})` : "";
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** A publication's table as `ADD TABLE` writes it: its name, column list and row filter. */
|
|
592
|
+
const publicationTable = (t: { table: string; columns?: string[]; where?: string }): string =>
|
|
593
|
+
`${t.table}${t.columns && t.columns.length > 0 ? ` (${t.columns.map(quoteIdent).join(", ")})` : ""}${t.where ? ` WHERE (${t.where})` : ""}`;
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* The ALTER statements that take a foreign data wrapper, a foreign server or
|
|
597
|
+
* a publication to the declaration, in place (SQLPG303): one ALTER for a
|
|
598
|
+
* wrapper or server; for a publication one SET (...) for its options, and a
|
|
599
|
+
* DROP and ADD per table or schema that changed.
|
|
600
|
+
*/
|
|
601
|
+
function alteredSteps(d: PgDiffObject, live: PgDiffObject, target: string): PgStep[] {
|
|
602
|
+
const out: string[] = [];
|
|
603
|
+
const f = d.fields;
|
|
604
|
+
const l = live.fields;
|
|
605
|
+
if (d.kind === "foreignDataWrapper") {
|
|
606
|
+
const clauses = [
|
|
607
|
+
!same(f.handler, l.handler) ? (f.handler ? `HANDLER ${String(f.handler)}` : "NO HANDLER") : "",
|
|
608
|
+
!same(f.validator, l.validator) ? (f.validator ? `VALIDATOR ${String(f.validator)}` : "NO VALIDATOR") : "",
|
|
609
|
+
optionsClause(f.options, l.options),
|
|
610
|
+
].filter(Boolean);
|
|
611
|
+
if (clauses.length > 0) out.push(`ALTER FOREIGN DATA WRAPPER ${target} ${clauses.join(" ")}`);
|
|
612
|
+
} else if (d.kind === "foreignServer") {
|
|
613
|
+
const clauses = [!same(f.version, l.version) ? `VERSION ${f.version === undefined ? "NULL" : pgString(String(f.version))}` : "", optionsClause(f.options, l.options)].filter(Boolean);
|
|
614
|
+
if (clauses.length > 0) out.push(`ALTER SERVER ${target} ${clauses.join(" ")}`);
|
|
615
|
+
} else if (d.kind === "publication") {
|
|
616
|
+
const sets: string[] = [];
|
|
617
|
+
if (!same(f.publish, l.publish)) sets.push(`publish = ${pgString(((f.publish as string[] | undefined) ?? ["insert", "update", "delete", "truncate"]).join(", "))}`);
|
|
618
|
+
if (!same(f.viaRoot, l.viaRoot)) sets.push(`publish_via_partition_root = ${f.viaRoot === true}`);
|
|
619
|
+
if (!same(f.generatedColumns, l.generatedColumns)) sets.push(`publish_generated_columns = ${String(f.generatedColumns ?? "none")}`);
|
|
620
|
+
if (sets.length > 0) out.push(`ALTER PUBLICATION ${target} SET (${sets.join(", ")})`);
|
|
621
|
+
type Member = { table: string; columns?: string[]; where?: string };
|
|
622
|
+
const want = (f.tables as Member[] | undefined) ?? [];
|
|
623
|
+
const have = (l.tables as Member[] | undefined) ?? [];
|
|
624
|
+
const kept = (m: Member) => want.some((w) => w.table === m.table && same(w, m));
|
|
625
|
+
for (const m of have) if (!kept(m)) out.push(`ALTER PUBLICATION ${target} DROP TABLE ${m.table}`);
|
|
626
|
+
for (const m of want) if (!have.some((h) => h.table === m.table && same(h, m))) out.push(`ALTER PUBLICATION ${target} ADD TABLE ${publicationTable(m)}`);
|
|
627
|
+
const wantSchemas = (f.schemas as string[] | undefined) ?? [];
|
|
628
|
+
const haveSchemas = (l.schemas as string[] | undefined) ?? [];
|
|
629
|
+
for (const s of haveSchemas) if (!wantSchemas.includes(s)) out.push(`ALTER PUBLICATION ${target} DROP TABLES IN SCHEMA ${s}`);
|
|
630
|
+
for (const s of wantSchemas) if (!haveSchemas.includes(s)) out.push(`ALTER PUBLICATION ${target} ADD TABLES IN SCHEMA ${s}`);
|
|
631
|
+
}
|
|
632
|
+
return out.map((sql) => step(sql, "metadata", "SQLPG303"));
|
|
633
|
+
}
|
|
634
|
+
|
|
572
635
|
/**
|
|
573
636
|
* The statements for an object that exists on the server, change by change.
|
|
574
637
|
* `live` is the server's definition, matched to the declaration (a renamed
|
|
@@ -704,6 +767,9 @@ export function alterSteps(
|
|
|
704
767
|
case "SQLPG229":
|
|
705
768
|
steps.push(step(`ALTER INDEX ${objectIdent(live)} RENAME TO ${quoteIdent(d.name)}`, c.class, c.rule));
|
|
706
769
|
break;
|
|
770
|
+
case "SQLPG303":
|
|
771
|
+
if (!steps.some((s) => s.rule === "SQLPG303")) steps.push(...alteredSteps(d, live, target));
|
|
772
|
+
break;
|
|
707
773
|
case "SQLPG250":
|
|
708
774
|
case "SQLPG280":
|
|
709
775
|
case "SQLPG302":
|
|
@@ -882,6 +948,9 @@ const DROP_ORDER: Record<CanonicalKind, number> = {
|
|
|
882
948
|
defaultPrivileges: 0,
|
|
883
949
|
policy: 0,
|
|
884
950
|
trigger: 0,
|
|
951
|
+
// A publication reads tables, extended statistics are on one: both go before it.
|
|
952
|
+
publication: 0,
|
|
953
|
+
statistics: 0,
|
|
885
954
|
materializedView: 1,
|
|
886
955
|
view: 2,
|
|
887
956
|
index: 3,
|
|
@@ -891,6 +960,10 @@ const DROP_ORDER: Record<CanonicalKind, number> = {
|
|
|
891
960
|
eventTrigger: 5.5,
|
|
892
961
|
cast: 5.5,
|
|
893
962
|
aggregate: 5.5,
|
|
963
|
+
operator: 5.5,
|
|
964
|
+
// A server before its wrapper, a wrapper before the handler function it calls.
|
|
965
|
+
foreignServer: 5.5,
|
|
966
|
+
foreignDataWrapper: 5.6,
|
|
894
967
|
// A routine after what calls it (a view, a trigger, a default), before the types its parameters use.
|
|
895
968
|
function: 6,
|
|
896
969
|
procedure: 6,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The names Postgres gives a constraint or an index that the DDL leaves
|
|
3
|
+
* unnamed, so a reader of the declarations can spell them without a server.
|
|
4
|
+
*
|
|
5
|
+
* `makeObjectName` and the `Choose*Name` functions are from the Postgres
|
|
6
|
+
* source (`src/backend/commands/indexcmds.c`, `src/backend/catalog/heap.c`,
|
|
7
|
+
* `src/backend/commands/tablecmds.c`): the table name, then the column
|
|
8
|
+
* names joined with `_`, then a label (`pkey`, `key`, `fkey`, `check`),
|
|
9
|
+
* cut to 63 bytes by shortening the longer of the first two parts one
|
|
10
|
+
* character at a time. A name already taken gets a number after the label
|
|
11
|
+
* (`orders_pkey1`).
|
|
12
|
+
*
|
|
13
|
+
* A name the author writes always wins; these are for the rest. Drizzle's
|
|
14
|
+
* own derived names (`orders_user_id_users_id_fk`) differ from them, which is
|
|
15
|
+
* why the Drizzle export names every constraint it writes.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const NAMEDATALEN = 64;
|
|
19
|
+
|
|
20
|
+
/** `makeObjectName(name1, name2, label)`: ASCII names only. */
|
|
21
|
+
export function makeObjectName(name1: string, name2: string | undefined, label: string | undefined): string {
|
|
22
|
+
let overhead = 0;
|
|
23
|
+
let n2 = 0;
|
|
24
|
+
if (name2 !== undefined) {
|
|
25
|
+
n2 = name2.length;
|
|
26
|
+
overhead++;
|
|
27
|
+
}
|
|
28
|
+
if (label !== undefined) overhead += label.length + 1;
|
|
29
|
+
const avail = NAMEDATALEN - 1 - overhead;
|
|
30
|
+
let n1 = name1.length;
|
|
31
|
+
while (n1 + n2 > avail) {
|
|
32
|
+
if (n1 > n2) n1--;
|
|
33
|
+
else n2--;
|
|
34
|
+
}
|
|
35
|
+
let name = name1.slice(0, n1);
|
|
36
|
+
if (name2 !== undefined) name += `_${name2.slice(0, n2)}`;
|
|
37
|
+
if (label !== undefined) name += `_${label}`;
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The first of `label`, `label1`, `label2`, ... that gives a name not in `taken`. */
|
|
42
|
+
export function chooseName(name1: string, name2: string | undefined, label: string, taken: ReadonlySet<string>): string {
|
|
43
|
+
let pass = 0;
|
|
44
|
+
let candidate = makeObjectName(name1, name2, label);
|
|
45
|
+
while (taken.has(candidate)) candidate = makeObjectName(name1, name2, `${label}${++pass}`);
|
|
46
|
+
return candidate;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The column names of a key or foreign key, joined as Postgres joins them. */
|
|
50
|
+
export const columnsAddition = (columns: readonly string[]): string => columns.join("_");
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The identifiers `expr` reads as column names: words outside quotes and
|
|
54
|
+
* string literals that are not followed by `(`, and are in `columns`.
|
|
55
|
+
*/
|
|
56
|
+
export function referencedColumns(expr: string, columns: readonly string[]): string[] {
|
|
57
|
+
const known = new Set(columns);
|
|
58
|
+
const found = new Set<string>();
|
|
59
|
+
const re = /'(?:[^']|'')*'|"((?:[^"]|"")+)"|([A-Za-z_][\w$]*)(?!\s*\()(?!\s*\.)/g;
|
|
60
|
+
for (let m = re.exec(expr); m; m = re.exec(expr)) {
|
|
61
|
+
const name = m[1] !== undefined ? m[1].replace(/""/g, '"') : m[2];
|
|
62
|
+
if (name !== undefined && known.has(m[1] !== undefined ? name : name.toLowerCase())) found.add(m[1] !== undefined ? name : name.toLowerCase());
|
|
63
|
+
}
|
|
64
|
+
return [...found];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A check constraint's default name: the column when the expression reads one, else just the table. */
|
|
68
|
+
export function checkName(table: string, expr: string, columns: readonly string[], taken: ReadonlySet<string>): string {
|
|
69
|
+
const refs = referencedColumns(expr, columns);
|
|
70
|
+
return chooseName(table, refs.length === 1 ? refs[0] : undefined, "check", taken);
|
|
71
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended statistics, foreign data wrappers, foreign servers and
|
|
3
|
+
* publications (#3911): what the tags parse and refuse, how a change to each
|
|
4
|
+
* is classified and made, how the catalog reader prints each, and the round
|
|
5
|
+
* trip: declare, build, import, plan with no change.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "vitest";
|
|
9
|
+
import { foreignDataWrapper, foreignServer, func, operator, publication, schema, SqlTemplateError, statistics, table } from "./entities";
|
|
10
|
+
import { diffObject, keyedByQualifiedName, pgSchemaFromLive, type PgSchemaObject } from "./plan/schema";
|
|
11
|
+
import { diffPgSchemas, matchPgObjects } from "./plan/diff";
|
|
12
|
+
import { alterSteps, dropStatement } from "./apply/statements";
|
|
13
|
+
import { declaredObjects } from "./apply/apply";
|
|
14
|
+
import { readLiveSchema, type LivePgObject } from "./live/catalog";
|
|
15
|
+
import { readDatabaseKinds, readOperators } from "./live/catalog-kinds";
|
|
16
|
+
import type { PostgresClient } from "./live/client";
|
|
17
|
+
import { objectsToIR, type ImportedPgObject } from "./import/ir";
|
|
18
|
+
import { PostgresGenerator, postgresTemplates } from "./import/generator";
|
|
19
|
+
import { canonicalPgObject } from "./plan/normalize";
|
|
20
|
+
import { liveProps } from "./plan/deep";
|
|
21
|
+
import { fakeClient } from "./testing/fake-client";
|
|
22
|
+
import { kindRow } from "../coverage";
|
|
23
|
+
|
|
24
|
+
const app = schema`CREATE SCHEMA app`;
|
|
25
|
+
const props = (o: { props: unknown }) => o.props as Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
describe("the tags", () => {
|
|
28
|
+
test("operator: a symbol in a schema, identified by its operand types", () => {
|
|
29
|
+
const eq = func`CREATE FUNCTION ${app}.pair_eq(a int, b int) RETURNS boolean LANGUAGE sql AS 'SELECT a = b'`;
|
|
30
|
+
const op = operator`CREATE OPERATOR ${app}.=== (FUNCTION = ${eq}, LEFTARG = int, RIGHTARG = int, COMMUTATOR = ===, NEGATOR = OPERATOR(app.!==), RESTRICT = eqsel, JOIN = eqjoinsel, HASHES); COMMENT ON OPERATOR ${app}.===(int, int) IS 'Same pair'`;
|
|
31
|
+
expect(op.entityType).toBe("Postgres::Operator");
|
|
32
|
+
expect(op.sqlName).toBe("app.===");
|
|
33
|
+
expect(op.dependsOn).toContain(eq);
|
|
34
|
+
expect(op.props).toMatchObject({ schema: "app", name: "===", leftArg: "int", rightArg: "int", comment: "Same pair" });
|
|
35
|
+
const c = canonicalPgObject(op.entityType, props(op));
|
|
36
|
+
expect(c.signature).toBe("(integer,integer)");
|
|
37
|
+
expect(c.fields.definition).toEqual({ commutator: "app.===", function: "app.pair_eq", hashes: "true", negator: "app.!==", restrict: "eqsel", join: "eqjoinsel" });
|
|
38
|
+
const prefix = operator`CREATE OPERATOR app.@@ (PROCEDURE = app.abs_of, RIGHTARG = int)`;
|
|
39
|
+
expect(canonicalPgObject(prefix.entityType, props(prefix))).toMatchObject({ name: "@@", signature: "(NONE,integer)", fields: { definition: { function: "app.abs_of" } } });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("statistics: a table's columns and expressions, the kinds the server gathers", () => {
|
|
43
|
+
const orders = table`CREATE TABLE ${app}.orders (id int, status text, total numeric)`;
|
|
44
|
+
const s = statistics`CREATE STATISTICS ${app}.orders_stats (dependencies, ndistinct) ON status, id, (total * 2) FROM ${orders}; COMMENT ON STATISTICS ${app}.orders_stats IS 'Correlated'`;
|
|
45
|
+
expect(s.entityType).toBe("Postgres::Statistics");
|
|
46
|
+
expect(s.dependsOn).toContain(orders);
|
|
47
|
+
expect(s.props).toMatchObject({ schema: "app", name: "orders_stats", kinds: ["dependencies", "ndistinct"], tableName: "app.orders", comment: "Correlated" });
|
|
48
|
+
// Sorted, and the kinds the server builds when none are written are no list at all.
|
|
49
|
+
const c = canonicalPgObject(s.entityType, props(s));
|
|
50
|
+
expect(c.fields).toMatchObject({ table: "app.orders", kinds: ["dependencies", "ndistinct"], targets: ["id", "status", "total * 2"] });
|
|
51
|
+
const all = statistics`CREATE STATISTICS app.s ON a, b FROM app.t`;
|
|
52
|
+
const listed = statistics`CREATE STATISTICS app.s (mcv, ndistinct, dependencies) ON b, a FROM app.t`;
|
|
53
|
+
expect(canonicalPgObject(all.entityType, props(all)).fields).toEqual(canonicalPgObject(listed.entityType, props(listed)).fields);
|
|
54
|
+
const lone = statistics`CREATE STATISTICS app.e ON (lower(email)) FROM app.t`;
|
|
55
|
+
expect(canonicalPgObject(lone.entityType, props(lone)).fields.kinds).toEqual(["none"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a foreign data wrapper and a foreign server, the database's and without a schema", () => {
|
|
59
|
+
const handler = func`CREATE FUNCTION ${app}.fdw_handler() RETURNS fdw_handler LANGUAGE c AS 'x', 'y'`;
|
|
60
|
+
const fdw = foreignDataWrapper`CREATE FOREIGN DATA WRAPPER app_fdw HANDLER ${handler} NO VALIDATOR OPTIONS (debug 'true'); COMMENT ON FOREIGN DATA WRAPPER app_fdw IS 'Remote'`;
|
|
61
|
+
expect(fdw.entityType).toBe("Postgres::ForeignDataWrapper");
|
|
62
|
+
expect(fdw.sqlName).toBe("app_fdw");
|
|
63
|
+
expect(fdw.dependsOn).toContain(handler);
|
|
64
|
+
expect(fdw.props).toMatchObject({ name: "app_fdw", handlerName: "app.fdw_handler", noValidator: true, options: { debug: "true" }, comment: "Remote" });
|
|
65
|
+
expect(canonicalPgObject(fdw.entityType, props(fdw))).toMatchObject({ schema: undefined, fields: { handler: "app.fdw_handler", options: { debug: "true" } } });
|
|
66
|
+
expect(canonicalPgObject(fdw.entityType, props(fdw)).fields).not.toHaveProperty("validator");
|
|
67
|
+
|
|
68
|
+
const server = foreignServer`CREATE SERVER IF NOT EXISTS app_remote TYPE 'pg' VERSION '16' FOREIGN DATA WRAPPER ${fdw} OPTIONS (host 'db.internal', port '5432'); COMMENT ON SERVER app_remote IS 'The replica'`;
|
|
69
|
+
expect(server.dependsOn).toContain(fdw);
|
|
70
|
+
expect(server.props).toMatchObject({ name: "app_remote", serverType: "pg", version: "16", wrapperName: "app_fdw", options: { host: "db.internal", port: "5432" }, comment: "The replica" });
|
|
71
|
+
expect(canonicalPgObject(server.entityType, props(server)).fields).toMatchObject({ serverType: "pg", version: "16", wrapper: "app_fdw", options: { host: "db.internal", port: "5432" } });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a publication: all tables, or tables with column lists and filters, or schemas, and its options", () => {
|
|
75
|
+
const orders = table`CREATE TABLE ${app}.orders (id int, status text)`;
|
|
76
|
+
const audit = schema`CREATE SCHEMA audit`;
|
|
77
|
+
const p = publication`CREATE PUBLICATION app_pub FOR TABLE ONLY ${orders} (status, id) WHERE (id > 0), app.users, TABLES IN SCHEMA ${audit} WITH (publish = 'update, insert', publish_via_partition_root = on)`;
|
|
78
|
+
expect(p.entityType).toBe("Postgres::Publication");
|
|
79
|
+
expect(p.dependsOn).toEqual(expect.arrayContaining([orders, audit]));
|
|
80
|
+
expect(canonicalPgObject(p.entityType, props(p)).fields).toMatchObject({
|
|
81
|
+
tables: [{ table: "app.orders", columns: ["id", "status"], where: "id > 0" }, { table: "app.users" }],
|
|
82
|
+
schemas: ["audit"],
|
|
83
|
+
publish: ["insert", "update"],
|
|
84
|
+
viaRoot: true,
|
|
85
|
+
});
|
|
86
|
+
const all = publication`CREATE PUBLICATION everything FOR ALL TABLES WITH (publish = 'insert, update, delete, truncate')`;
|
|
87
|
+
expect(canonicalPgObject(all.entityType, props(all)).fields).toEqual({ allTables: true });
|
|
88
|
+
expect(canonicalPgObject(all.entityType, props(all)).fields.publish).toBeUndefined();
|
|
89
|
+
const empty = publication`CREATE PUBLICATION nothing`;
|
|
90
|
+
expect(canonicalPgObject(empty.entityType, props(empty)).fields).not.toHaveProperty("tables");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("what is refused", () => {
|
|
94
|
+
expect(() => operator`CREATE OPERATOR app.=== (LEFTARG = int, RIGHTARG = int)`).toThrow(/FUNCTION/);
|
|
95
|
+
expect(() => operator`CREATE OPERATOR app.=== (FUNCTION = f, LEFTARG = int, MERGE = true)`).toThrow(/an option of an operator/);
|
|
96
|
+
expect(() => operator`CREATE OPERATOR CLASS c FOR TYPE int USING btree AS OPERATOR 1 <`).toThrow(/CREATE OPERATOR CLASS and CREATE OPERATOR FAMILY/);
|
|
97
|
+
expect(() => operator`CREATE OPERATOR app.eq (FUNCTION = f)`).toThrow(/operator's symbol/);
|
|
98
|
+
expect(() => foreignServer`CREATE SERVER s FOREIGN DATA WRAPPER w OPTIONS (host 'x', password 'hunter2')`).toThrow(/holds a credential/);
|
|
99
|
+
expect(() => foreignDataWrapper`CREATE FOREIGN DATA WRAPPER w OPTIONS (api_key 'x')`).toThrow(SqlTemplateError);
|
|
100
|
+
expect(() => foreignServer`CREATE SERVER s OPTIONS (host 'x')`).toThrow(/FOREIGN DATA WRAPPER/);
|
|
101
|
+
expect(() => foreignServer`CREATE SERVER app.s FOREIGN DATA WRAPPER w`).toThrow(/without a schema/);
|
|
102
|
+
expect(() => foreignServer`CREATE SERVER s FOREIGN DATA WRAPPER w OPTIONS (host 'a', host 'b')`).toThrow(/given twice/);
|
|
103
|
+
expect(() => publication`CREATE PUBLICATION p WITH (publich = 'insert')`).toThrow(/an option of a publication/);
|
|
104
|
+
expect(() => publication`CREATE PUBLICATION p FOR`).toThrow();
|
|
105
|
+
expect(() => statistics`CREATE STATISTICS app.s (histogram) ON a, b FROM app.t`).toThrow(/statistics kind/);
|
|
106
|
+
expect(() => statistics`CREATE STATISTICS app.s ON a, b`).toThrow(/FROM/);
|
|
107
|
+
expect(() => statistics`CREATE USER MAPPING FOR u SERVER s`).toThrow(SqlTemplateError);
|
|
108
|
+
expect(() => publication`CREATE STATISTICS app.s ON a, b FROM app.t`).toThrow(/use the statistics tag/);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── Classifying and making changes ─────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
const live = (type: string, statement: string, extra: Partial<LivePgObject> = {}) => pgSchemaFromLive([{ type, statement, name: "", oid: "1", ...extra } as LivePgObject], "public");
|
|
115
|
+
const declared = (type: string, ddl: string): PgSchemaObject[] => keyedByQualifiedName([{ key: "x", canonical: { ...diffObject(type, ddl, "public"), exportName: "x" } }]);
|
|
116
|
+
const rules = (type: string, before: string, after: string, extra: Partial<LivePgObject> = {}) =>
|
|
117
|
+
diffPgSchemas(live(type, before, extra), declared(type, after)).changes.map((c) => [c.field, c.rule]);
|
|
118
|
+
|
|
119
|
+
function steps(type: string, liveDdl: string, declaredDdl: string, extra: Partial<LivePgObject> = {}): string[] {
|
|
120
|
+
const [obj] = declaredObjects(JSON.stringify({ dialect: "postgres", objects: [{ export: "x", type, ddl: declaredDdl, dependsOn: [] }] }));
|
|
121
|
+
const after = keyedByQualifiedName([{ key: "x", canonical: obj!.canonical }]);
|
|
122
|
+
const before = live(type, liveDdl, extra);
|
|
123
|
+
const m = matchPgObjects(before, after).find((x) => x.kind === "matched")!;
|
|
124
|
+
return alterSteps(obj!, m.before!.canonical, diffPgSchemas(before, after).changes, { major: 18 }).steps.map((s) => s.sql);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const OPR = "Postgres::Operator";
|
|
128
|
+
const STAT = "Postgres::Statistics";
|
|
129
|
+
const FDW = "Postgres::ForeignDataWrapper";
|
|
130
|
+
const SRV = "Postgres::ForeignServer";
|
|
131
|
+
const PUB = "Postgres::Publication";
|
|
132
|
+
|
|
133
|
+
describe("the classifier and the statements", () => {
|
|
134
|
+
test("the server's printing is the declaration's", () => {
|
|
135
|
+
expect(rules(STAT, "CREATE STATISTICS app.s ON a, b FROM app.t", "CREATE STATISTICS app.s (ndistinct, dependencies, mcv) ON b, a FROM app.t")).toEqual([]);
|
|
136
|
+
expect(rules(STAT, "CREATE STATISTICS app.s (dependencies) ON a, (b + 1) FROM app.t", "CREATE STATISTICS app.s (dependencies) ON (b + 1), a FROM app.t")).toEqual([]);
|
|
137
|
+
expect(rules(FDW, "CREATE FOREIGN DATA WRAPPER w HANDLER app.h OPTIONS (a '1', b '2')", "CREATE FOREIGN DATA WRAPPER w HANDLER app.h NO VALIDATOR OPTIONS (b '2', a '1')")).toEqual([]);
|
|
138
|
+
expect(rules(SRV, "CREATE SERVER s TYPE 'pg' FOREIGN DATA WRAPPER w OPTIONS (host 'h')", "CREATE SERVER s TYPE 'pg' FOREIGN DATA WRAPPER w OPTIONS (host 'h')")).toEqual([]);
|
|
139
|
+
expect(
|
|
140
|
+
rules(PUB, "CREATE PUBLICATION p FOR TABLE app.a (id), app.b, TABLES IN SCHEMA s WITH (publish = 'insert, update')", "CREATE PUBLICATION p FOR TABLE app.b, app.a (id), TABLES IN SCHEMA s WITH (publish = 'update,insert')"),
|
|
141
|
+
).toEqual([]);
|
|
142
|
+
// Options at their default are no change: the publication publishes every action, to its own tables, unless told otherwise.
|
|
143
|
+
expect(rules(PUB, "CREATE PUBLICATION p FOR ALL TABLES", "CREATE PUBLICATION p FOR ALL TABLES WITH (publish = 'insert, update, delete, truncate', publish_via_partition_root = false)")).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("an operator is dropped and created: it has no ALTER for its function or operands", () => {
|
|
147
|
+
const O1 = "CREATE OPERATOR app.=== (FUNCTION = app.pair_eq, LEFTARG = integer, RIGHTARG = integer, COMMUTATOR = OPERATOR(app.===), RESTRICT = eqsel)";
|
|
148
|
+
expect(rules(OPR, O1, "CREATE OPERATOR app.=== (LEFTARG = int, RIGHTARG = int, RESTRICT = eqsel, COMMUTATOR = ===, FUNCTION = app.pair_eq)")).toEqual([]);
|
|
149
|
+
expect(rules(OPR, O1, O1.replace("eqsel", "scalarltsel"))).toEqual([["definition", "SQLPG300"]]);
|
|
150
|
+
expect(rules(OPR, O1, O1.replace("eqsel", "scalarltsel"), { dependents: ["operator class app.c for access method btree"] })).toEqual([["definition", "SQLPG301"]]);
|
|
151
|
+
const recreate = steps(OPR, O1, O1.replace("eqsel", "scalarltsel"));
|
|
152
|
+
expect(recreate[0]).toBe("DROP OPERATOR app.===(integer,integer)");
|
|
153
|
+
expect(recreate[1]).toContain("scalarltsel");
|
|
154
|
+
expect(recreate[2]).toMatch(/^COMMENT ON OPERATOR app\.===\(integer,integer\) IS /);
|
|
155
|
+
// Another operand type is another operator, not a change.
|
|
156
|
+
expect(dropStatement("operator", "app", "===", "(NONE,integer)").sql).toBe("DROP OPERATOR app.===(NONE,integer)");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("statistics are dropped and created: they have no ALTER for their definition", () => {
|
|
160
|
+
const S1 = "CREATE STATISTICS app.s ON a, b FROM app.t";
|
|
161
|
+
const S2 = "CREATE STATISTICS app.s (dependencies) ON a, b FROM app.t";
|
|
162
|
+
expect(rules(STAT, S1, S2)).toEqual([["kinds", "SQLPG300"]]);
|
|
163
|
+
expect(rules(STAT, S1, "CREATE STATISTICS app.s ON a, c FROM app.t")).toEqual([["targets", "SQLPG300"]]);
|
|
164
|
+
expect(rules(STAT, S1, "CREATE STATISTICS app.s ON a, b FROM app.u")).toEqual([["table", "SQLPG300"]]);
|
|
165
|
+
const recreate = steps(STAT, S1, S2);
|
|
166
|
+
expect(recreate[0]).toBe("DROP STATISTICS app.s");
|
|
167
|
+
expect(recreate[1]).toBe(S2);
|
|
168
|
+
expect(recreate[2]).toMatch(/^COMMENT ON STATISTICS app\.s IS /);
|
|
169
|
+
expect(dropStatement("statistics", "app", "s").sql).toBe("DROP STATISTICS app.s");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("a wrapper is altered in place", () => {
|
|
173
|
+
const W1 = "CREATE FOREIGN DATA WRAPPER w HANDLER app.h OPTIONS (a '1', b '2')";
|
|
174
|
+
const W2 = "CREATE FOREIGN DATA WRAPPER w NO HANDLER VALIDATOR app.v OPTIONS (a '9', c '3')";
|
|
175
|
+
expect(rules(FDW, W1, W2)).toEqual([
|
|
176
|
+
["handler", "SQLPG303"],
|
|
177
|
+
["validator", "SQLPG303"],
|
|
178
|
+
["options", "SQLPG303"],
|
|
179
|
+
]);
|
|
180
|
+
expect(steps(FDW, W1, W2)).toEqual(["ALTER FOREIGN DATA WRAPPER w NO HANDLER VALIDATOR app.v OPTIONS (ADD c '3', SET a '9', DROP b)"]);
|
|
181
|
+
expect(dropStatement("foreignDataWrapper", undefined, "w").sql).toBe("DROP FOREIGN DATA WRAPPER w");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("a server's version and options are altered in place; its type and wrapper are not", () => {
|
|
185
|
+
const V1 = "CREATE SERVER s TYPE 'pg' VERSION '15' FOREIGN DATA WRAPPER w OPTIONS (host 'a')";
|
|
186
|
+
expect(rules(SRV, V1, V1.replace("'15'", "'16'").replace("'a'", "'b'"))).toEqual([
|
|
187
|
+
["version", "SQLPG303"],
|
|
188
|
+
["options", "SQLPG303"],
|
|
189
|
+
]);
|
|
190
|
+
expect(steps(SRV, V1, V1.replace("VERSION '15' ", "").replace("'a'", "'b'"))).toEqual(["ALTER SERVER s VERSION NULL OPTIONS (SET host 'b')"]);
|
|
191
|
+
expect(rules(SRV, V1, V1.replace("w OPTIONS", "w2 OPTIONS"))).toEqual([["wrapper", "SQLPG300"]]);
|
|
192
|
+
expect(rules(SRV, V1, V1.replace("w OPTIONS", "w2 OPTIONS"), { dependents: ["foreign table app.remote_orders"] })).toEqual([["wrapper", "SQLPG301"]]);
|
|
193
|
+
const recreate = steps(SRV, V1, V1.replace("'pg'", "'mysql'"));
|
|
194
|
+
expect(recreate[0]).toBe("DROP SERVER s");
|
|
195
|
+
expect(recreate[1]).toMatch(/^CREATE SERVER s TYPE 'mysql'/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("a publication is altered in place: options, tables and schemas", () => {
|
|
199
|
+
const P1 = "CREATE PUBLICATION p FOR TABLE app.a (id), app.b, TABLES IN SCHEMA s1";
|
|
200
|
+
const P2 = "CREATE PUBLICATION p FOR TABLE app.a (id, name) WHERE (id > 0), app.c, TABLES IN SCHEMA s2 WITH (publish = 'insert', publish_via_partition_root = true)";
|
|
201
|
+
expect(rules(PUB, P1, P2).map(([f]) => f)).toEqual(["tables", "schemas", "publish", "viaRoot"]);
|
|
202
|
+
expect(steps(PUB, P1, P2)).toEqual([
|
|
203
|
+
"ALTER PUBLICATION p SET (publish = 'insert', publish_via_partition_root = true)",
|
|
204
|
+
"ALTER PUBLICATION p DROP TABLE app.a",
|
|
205
|
+
"ALTER PUBLICATION p DROP TABLE app.b",
|
|
206
|
+
"ALTER PUBLICATION p ADD TABLE app.a (id, name) WHERE (id > 0)",
|
|
207
|
+
"ALTER PUBLICATION p ADD TABLE app.c",
|
|
208
|
+
"ALTER PUBLICATION p DROP TABLES IN SCHEMA s1",
|
|
209
|
+
"ALTER PUBLICATION p ADD TABLES IN SCHEMA s2",
|
|
210
|
+
]);
|
|
211
|
+
// Back to the defaults is written out: the server has no RESET for a publication's options.
|
|
212
|
+
expect(steps(PUB, "CREATE PUBLICATION p WITH (publish = 'insert', publish_via_partition_root = true)", "CREATE PUBLICATION p")).toEqual([
|
|
213
|
+
"ALTER PUBLICATION p SET (publish = 'insert, update, delete, truncate', publish_via_partition_root = false)",
|
|
214
|
+
]);
|
|
215
|
+
// FOR ALL TABLES cannot be altered into or out of.
|
|
216
|
+
expect(rules(PUB, "CREATE PUBLICATION p FOR ALL TABLES", "CREATE PUBLICATION p FOR TABLE app.a")).toEqual([["allTables", "SQLPG300"]]);
|
|
217
|
+
expect(steps(PUB, "CREATE PUBLICATION p FOR ALL TABLES", "CREATE PUBLICATION p FOR TABLE app.a")[0]).toBe("DROP PUBLICATION p");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("a comment changes in place", () => {
|
|
221
|
+
expect(steps(SRV, "CREATE SERVER s FOREIGN DATA WRAPPER w", "CREATE SERVER s FOREIGN DATA WRAPPER w; COMMENT ON SERVER s IS 'x'")).toEqual([expect.stringMatching(/^COMMENT ON SERVER s IS 'x/)]);
|
|
222
|
+
expect(steps(PUB, "CREATE PUBLICATION p", "CREATE PUBLICATION p; COMMENT ON PUBLICATION p IS 'x'")).toEqual([expect.stringMatching(/^COMMENT ON PUBLICATION p IS 'x/)]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ── Reading the catalog ────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
/** Answers the database-kind reader's queries from rows, by the catalog each reads. */
|
|
229
|
+
function catalogClient(rows: { stats?: unknown[]; wrappers?: unknown[]; servers?: unknown[]; pubs?: unknown[]; pubTables?: unknown[]; pubSchemas?: unknown[]; version15?: boolean }): PostgresClient {
|
|
230
|
+
return {
|
|
231
|
+
async query<T>(sql: string): Promise<T[]> {
|
|
232
|
+
const r = (x: unknown[] | undefined) => (x ?? []) as T[];
|
|
233
|
+
if (sql.includes("pg_get_statisticsobjdef")) return r(rows.stats);
|
|
234
|
+
if (sql.includes("FROM pg_catalog.pg_foreign_data_wrapper w")) return r(rows.wrappers);
|
|
235
|
+
if (sql.includes("FROM pg_catalog.pg_foreign_server v")) return r(rows.servers);
|
|
236
|
+
if (sql.includes("FROM pg_catalog.pg_publication p")) return r(rows.pubs);
|
|
237
|
+
if (sql.includes("FROM pg_catalog.pg_publication_rel r")) return r(rows.pubTables);
|
|
238
|
+
if (sql.includes("FROM pg_catalog.pg_publication_namespace pn")) return r(rows.pubSchemas);
|
|
239
|
+
// The 15 features: the column list, and schema members.
|
|
240
|
+
if (sql.includes("attname = 'prattrs'") || sql.includes("to_regclass('pg_catalog.pg_publication_namespace')")) return rows.version15 ? r([{ x: 1 }]) : [];
|
|
241
|
+
return [];
|
|
242
|
+
},
|
|
243
|
+
async end() {},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
describe("reading the catalog", () => {
|
|
248
|
+
test("each kind is assembled from its rows, a credential left out and said so", async () => {
|
|
249
|
+
const objects = await readDatabaseKinds(
|
|
250
|
+
catalogClient({
|
|
251
|
+
version15: true,
|
|
252
|
+
stats: [{ oid: "1", schema: "app", name: "s", def: "CREATE STATISTICS app.s (dependencies) ON a, b FROM app.t", comment: "Linked" }],
|
|
253
|
+
wrappers: [{ oid: "2", name: "app_fdw", handler: "app.h", validator: null, options: ["debug=true"], comment: null, dependents: ["server app_remote"] }],
|
|
254
|
+
servers: [{ oid: "3", name: "app_remote", type: "pg", version: null, wrapper: "app_fdw", options: ["host=db", "sslpassword=x", "password=y"], comment: "It's remote", dependents: [] }],
|
|
255
|
+
pubs: [{ oid: "4", name: "app_pub", alltables: false, pubinsert: true, pubupdate: true, pubdelete: false, pubtruncate: false, def: { pubviaroot: true, pubgencols: "n" }, comment: null }],
|
|
256
|
+
pubTables: [{ schema: "app", name: "t", columns: ['"a"', '"b"'], filter: "a > 0" }, { schema: "app", name: "u", columns: [], filter: null }],
|
|
257
|
+
pubSchemas: [{ name: "audit" }],
|
|
258
|
+
}),
|
|
259
|
+
{},
|
|
260
|
+
);
|
|
261
|
+
expect(objects.map((o) => [o.type, o.name])).toEqual([
|
|
262
|
+
[STAT, "s"],
|
|
263
|
+
[FDW, "app_fdw"],
|
|
264
|
+
[SRV, "app_remote"],
|
|
265
|
+
[PUB, "app_pub"],
|
|
266
|
+
]);
|
|
267
|
+
expect(objects[0]!.statement).toBe("CREATE STATISTICS app.s (dependencies) ON a, b FROM app.t;\nCOMMENT ON STATISTICS app.s IS 'Linked'");
|
|
268
|
+
expect(objects[1]).toMatchObject({ statement: "CREATE FOREIGN DATA WRAPPER app_fdw HANDLER app.h OPTIONS (debug 'true')", dependents: ["server app_remote"] });
|
|
269
|
+
expect(objects[2]!.statement).toBe("CREATE SERVER app_remote TYPE 'pg' FOREIGN DATA WRAPPER app_fdw OPTIONS (host 'db');\nCOMMENT ON SERVER app_remote IS 'It''s remote'");
|
|
270
|
+
expect(objects[2]!.warnings).toEqual(["the options sslpassword, password hold a credential and are left out; the environment supplies it"]);
|
|
271
|
+
expect(objects[3]!.statement).toBe(
|
|
272
|
+
"CREATE PUBLICATION app_pub FOR TABLE app.t (\"a\", \"b\") WHERE (a > 0), app.u, TABLES IN SCHEMA audit WITH (publish = 'insert, update', publish_via_partition_root = true)",
|
|
273
|
+
);
|
|
274
|
+
// Each parses back with its own tag.
|
|
275
|
+
for (const o of objects) expect(() => liveProps(o)).not.toThrow();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("an operator: its operands, its commutator and its flags, as the catalog holds them", async () => {
|
|
279
|
+
const client: PostgresClient = {
|
|
280
|
+
async query<T>(sql: string): Promise<T[]> {
|
|
281
|
+
if (!sql.includes("FROM pg_catalog.pg_operator o")) return [];
|
|
282
|
+
return [
|
|
283
|
+
{ oid: "1", schema: "app", name: "===", leftarg: "integer", rightarg: "integer", func: "app.pair_eq", commutator: "OPERATOR(app.===)", negator: null, restrict: "eqsel", join_func: null, hashes: true, merges: false, comment: "Same", dependents: ["operator class app.c"] },
|
|
284
|
+
{ oid: "2", schema: "app", name: "@@", leftarg: null, rightarg: "integer", func: "app.abs_of", commutator: null, negator: null, restrict: null, join_func: null, hashes: false, merges: false, comment: null, dependents: [] },
|
|
285
|
+
] as T[];
|
|
286
|
+
},
|
|
287
|
+
async end() {},
|
|
288
|
+
};
|
|
289
|
+
const ops = await readOperators(client, {});
|
|
290
|
+
expect(ops.map((o) => [o.schema, o.name, o.signature])).toEqual([["app", "===", "(integer,integer)"], ["app", "@@", "(NONE,integer)"]]);
|
|
291
|
+
expect(ops[0]!.statement).toBe(
|
|
292
|
+
"CREATE OPERATOR app.=== (FUNCTION = app.pair_eq, LEFTARG = integer, RIGHTARG = integer, COMMUTATOR = OPERATOR(app.===), RESTRICT = eqsel, HASHES);\nCOMMENT ON OPERATOR app.===(integer,integer) IS 'Same'",
|
|
293
|
+
);
|
|
294
|
+
expect(ops[0]!.dependents).toEqual(["operator class app.c"]);
|
|
295
|
+
expect(ops[1]!.statement).toBe("CREATE OPERATOR app.@@ (FUNCTION = app.abs_of, RIGHTARG = integer)");
|
|
296
|
+
for (const o of ops) expect(() => liveProps(o)).not.toThrow();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("a publication of every table, on a server before 15", async () => {
|
|
300
|
+
const [p] = await readDatabaseKinds(catalogClient({ pubs: [{ oid: "4", name: "all_of_it", alltables: true, pubinsert: true, pubupdate: true, pubdelete: true, pubtruncate: true, def: { pubviaroot: false }, comment: null }] }), {});
|
|
301
|
+
expect(p!.statement).toBe("CREATE PUBLICATION all_of_it FOR ALL TABLES");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("the schema reader includes them, after the functions they call", async () => {
|
|
305
|
+
const log: string[] = [];
|
|
306
|
+
await readLiveSchema(fakeClient({}, log), { schemas: ["app"] });
|
|
307
|
+
expect(log.some((q) => q.includes("pg_get_statisticsobjdef"))).toBe(true);
|
|
308
|
+
expect(log.some((q) => q.includes("pg_foreign_server"))).toBe(true);
|
|
309
|
+
expect(log.some((q) => q.includes("FROM pg_catalog.pg_publication p"))).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ── The round trip ─────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
/** What the server prints for the objects below. */
|
|
316
|
+
const SERVER_PRINTS: Array<{ type: string; schema?: string; name: string; signature?: string; statement: string }> = [
|
|
317
|
+
{ type: "Postgres::Schema", name: "app", statement: "CREATE SCHEMA app" },
|
|
318
|
+
{ type: "Postgres::Schema", name: "audit", statement: "CREATE SCHEMA audit" },
|
|
319
|
+
{ type: "Postgres::Table", schema: "app", name: "orders", statement: "CREATE TABLE app.orders (\n id integer,\n status text\n)" },
|
|
320
|
+
{ type: "Postgres::Function", schema: "app", name: "pair_eq", signature: "(integer,integer)", statement: "CREATE FUNCTION app.pair_eq(a integer, b integer)\n RETURNS boolean\n LANGUAGE sql\nAS $function$SELECT a = b$function$" },
|
|
321
|
+
{ type: OPR, schema: "app", name: "===", signature: "(integer,integer)", statement: "CREATE OPERATOR app.=== (FUNCTION = app.pair_eq, LEFTARG = integer, RIGHTARG = integer, COMMUTATOR = OPERATOR(app.===), HASHES);\nCOMMENT ON OPERATOR app.===(integer,integer) IS 'Same pair'" },
|
|
322
|
+
{ type: STAT, schema: "app", name: "orders_stats", statement: "CREATE STATISTICS app.orders_stats (dependencies) ON id, status FROM app.orders;\nCOMMENT ON STATISTICS app.orders_stats IS 'Correlated'" },
|
|
323
|
+
{ type: FDW, name: "app_fdw", statement: "CREATE FOREIGN DATA WRAPPER app_fdw OPTIONS (debug 'true');\nCOMMENT ON FOREIGN DATA WRAPPER app_fdw IS 'Remote'" },
|
|
324
|
+
{ type: SRV, name: "app_remote", statement: "CREATE SERVER app_remote TYPE 'pg' VERSION '16' FOREIGN DATA WRAPPER app_fdw OPTIONS (host 'db.internal', port '5432')" },
|
|
325
|
+
{ type: PUB, name: "app_pub", statement: "CREATE PUBLICATION app_pub FOR TABLE app.orders (id, status) WHERE (id > 0), TABLES IN SCHEMA audit WITH (publish = 'insert, update')" },
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
describe("declare, build, import, plan with no change", () => {
|
|
329
|
+
const liveObjects = (): LivePgObject[] => SERVER_PRINTS.map((o, i) => ({ oid: String(i), ...o }) as LivePgObject);
|
|
330
|
+
|
|
331
|
+
test("a declaration the server prints back plans with no change", () => {
|
|
332
|
+
const orders = table`CREATE TABLE ${app}.orders (id int, status text)`;
|
|
333
|
+
const audit = schema`CREATE SCHEMA audit`;
|
|
334
|
+
const fdw = foreignDataWrapper`CREATE FOREIGN DATA WRAPPER app_fdw OPTIONS (debug 'true'); COMMENT ON FOREIGN DATA WRAPPER app_fdw IS 'Remote'`;
|
|
335
|
+
const pairEq = func`CREATE FUNCTION ${app}.pair_eq(a integer, b integer) RETURNS boolean LANGUAGE sql AS 'SELECT a = b'`;
|
|
336
|
+
const objects = [
|
|
337
|
+
app,
|
|
338
|
+
audit,
|
|
339
|
+
orders,
|
|
340
|
+
pairEq,
|
|
341
|
+
operator`CREATE OPERATOR ${app}.=== (LEFTARG = int, RIGHTARG = int, FUNCTION = ${pairEq}, HASHES, COMMUTATOR = ===); COMMENT ON OPERATOR ${app}.===(int, int) IS 'Same pair'`,
|
|
342
|
+
statistics`CREATE STATISTICS ${app}.orders_stats (dependencies) ON status, id FROM ${orders}; COMMENT ON STATISTICS ${app}.orders_stats IS 'Correlated'`,
|
|
343
|
+
fdw,
|
|
344
|
+
foreignServer`CREATE SERVER app_remote TYPE 'pg' VERSION '16' FOREIGN DATA WRAPPER ${fdw} OPTIONS (port '5432', host 'db.internal')`,
|
|
345
|
+
publication`CREATE PUBLICATION app_pub FOR TABLE ${orders} (status, id) WHERE (id > 0), TABLES IN SCHEMA ${audit} WITH (publish = 'update, insert')`,
|
|
346
|
+
];
|
|
347
|
+
// The build: each entity's DDL, rendered, as the build output holds it.
|
|
348
|
+
const build = JSON.stringify({ dialect: "postgres", objects: objects.map((o, i) => ({ export: `o${i}`, type: o.entityType, ddl: (o.props as { ddl: string }).ddl, dependsOn: [] })) });
|
|
349
|
+
const declaredSchema = keyedByQualifiedName(declaredObjects(build).map((d) => ({ key: d.exportName, canonical: d.canonical })));
|
|
350
|
+
expect(diffPgSchemas(pgSchemaFromLive(liveObjects(), "public"), declaredSchema).changes).toEqual([]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("import writes each as a declaration, which builds to the same objects", () => {
|
|
354
|
+
const ir = objectsToIR(SERVER_PRINTS.map((o) => ({ type: o.type, ...(o.schema ? { schema: o.schema } : {}), name: o.name, ddl: o.statement }) as ImportedPgObject));
|
|
355
|
+
const [file] = new PostgresGenerator().generate(ir);
|
|
356
|
+
const content = file!.content;
|
|
357
|
+
expect(content).toContain('import { foreignDataWrapper, foreignServer, func, operator, publication, schema, statistics, table } from "@intentius/chant-lexicon-sql/postgres";');
|
|
358
|
+
expect(content).toContain("export const ordersStats = statistics`");
|
|
359
|
+
expect(content).toContain("export const pairEq = func`");
|
|
360
|
+
expect(content).toContain("CREATE OPERATOR ${appSchema}.=== (FUNCTION = ${pairEq}, LEFTARG = integer");
|
|
361
|
+
expect(content).toContain("CREATE STATISTICS ${appSchema}.orders_stats (dependencies) ON id, status FROM ${orders}");
|
|
362
|
+
expect(content).toContain("export const appFdw = foreignDataWrapper`");
|
|
363
|
+
expect(content).toContain("FOREIGN DATA WRAPPER ${appFdw} OPTIONS (host 'db.internal'");
|
|
364
|
+
expect(content).toContain("export const appPub = publication`");
|
|
365
|
+
expect(content).toContain("TABLES IN SCHEMA ${auditSchema} WITH (publish = 'insert, update')");
|
|
366
|
+
expect(content).toContain("FOR TABLE ${orders} (id, status) WHERE (id > 0)");
|
|
367
|
+
|
|
368
|
+
// The imported statements, in the generator's order, as a build: no change against the objects they came from.
|
|
369
|
+
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) }));
|
|
370
|
+
const { order } = postgresTemplates(items);
|
|
371
|
+
expect(order.map((o) => o.exportName).indexOf("appFdw")).toBeLessThan(order.map((o) => o.exportName).indexOf("appRemote"));
|
|
372
|
+
expect(order.map((o) => o.exportName).indexOf("auditSchema")).toBeLessThan(order.map((o) => o.exportName).indexOf("appPub"));
|
|
373
|
+
const build = JSON.stringify({ dialect: "postgres", objects: order.map((it) => ({ export: it.exportName, type: it.type, ddl: it.ddl, dependsOn: [] })) });
|
|
374
|
+
const declaredSchema = keyedByQualifiedName(declaredObjects(build).map((d) => ({ key: d.exportName, canonical: d.canonical })));
|
|
375
|
+
expect(diffPgSchemas(pgSchemaFromLive(liveObjects(), "public"), declaredSchema).changes).toEqual([]);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("the kinds that stay refused", () => {
|
|
380
|
+
test("a user mapping and a subscription say why: they carry credentials", () => {
|
|
381
|
+
expect(kindRow("postgres", "user mapping")).toMatchObject({ status: "refused", reason: expect.stringContaining("password") });
|
|
382
|
+
expect(kindRow("postgres", "subscription")).toMatchObject({ status: "refused", reason: expect.stringContaining("credential") });
|
|
383
|
+
});
|
|
384
|
+
});
|