@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
|
@@ -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
|
|
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,
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
}
|