@supabase/pg-delta 1.0.0-alpha.4 → 1.0.0-alpha.5
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/README.md +40 -23
- package/dist/cli/app.js +26 -3
- package/dist/cli/bin/cli.js +5 -0
- package/dist/cli/commands/catalog-export.d.ts +5 -0
- package/dist/cli/commands/catalog-export.js +64 -0
- package/dist/cli/commands/declarative-apply.d.ts +6 -0
- package/dist/cli/commands/declarative-apply.js +288 -0
- package/dist/cli/commands/declarative-export.d.ts +5 -0
- package/dist/cli/commands/declarative-export.js +245 -0
- package/dist/cli/commands/plan.js +19 -6
- package/dist/cli/exit-code.d.ts +2 -0
- package/dist/cli/exit-code.js +7 -0
- package/dist/cli/formatters/tree/tree.js +3 -2
- package/dist/cli/utils/apply-display.d.ts +52 -0
- package/dist/cli/utils/apply-display.js +183 -0
- package/dist/cli/utils/export-display.d.ts +43 -0
- package/dist/cli/utils/export-display.js +202 -0
- package/dist/cli/utils/resolve-input.d.ts +7 -0
- package/dist/cli/utils/resolve-input.js +13 -0
- package/dist/core/catalog-export/index.d.ts +11 -0
- package/dist/core/catalog-export/index.js +10 -0
- package/dist/core/catalog.diff.d.ts +1 -0
- package/dist/core/catalog.diff.js +64 -48
- package/dist/core/catalog.model.d.ts +14 -1
- package/dist/core/catalog.model.js +103 -1
- package/dist/core/catalog.snapshot.d.ts +66 -0
- package/dist/core/catalog.snapshot.js +206 -0
- package/dist/core/declarative-apply/discover-sql.d.ts +18 -0
- package/dist/core/declarative-apply/discover-sql.js +86 -0
- package/dist/core/declarative-apply/extract-catalog-providers.d.ts +23 -0
- package/dist/core/declarative-apply/extract-catalog-providers.js +159 -0
- package/dist/core/declarative-apply/index.d.ts +49 -0
- package/dist/core/declarative-apply/index.js +134 -0
- package/dist/core/declarative-apply/round-apply.d.ts +100 -0
- package/dist/core/declarative-apply/round-apply.js +378 -0
- package/dist/core/export/file-mapper.d.ts +71 -0
- package/dist/core/export/file-mapper.js +474 -0
- package/dist/core/export/grouper.d.ts +13 -0
- package/dist/core/export/grouper.js +76 -0
- package/dist/core/export/index.d.ts +45 -0
- package/dist/core/export/index.js +63 -0
- package/dist/core/export/types.d.ts +84 -0
- package/dist/core/export/types.js +25 -0
- package/dist/core/fixtures/empty-catalogs/postgres-15-16-baseline.json +287 -0
- package/dist/core/integrations/filter/dsl.d.ts +38 -1
- package/dist/core/integrations/filter/dsl.js +20 -2
- package/dist/core/integrations/filter/extractors.js +42 -0
- package/dist/core/integrations/integration-dsl.d.ts +10 -0
- package/dist/core/integrations/supabase.d.ts +8 -0
- package/dist/core/integrations/supabase.js +9 -0
- package/dist/core/objects/aggregate/aggregate.diff.d.ts +2 -8
- package/dist/core/objects/aggregate/aggregate.diff.js +16 -70
- package/dist/core/objects/aggregate/aggregate.model.d.ts +8 -8
- package/dist/core/objects/aggregate/aggregate.model.js +1 -1
- package/dist/core/objects/aggregate/changes/aggregate.create.js +1 -1
- package/dist/core/objects/aggregate/changes/aggregate.drop.js +1 -1
- package/dist/core/objects/base.privilege-diff.d.ts +38 -13
- package/dist/core/objects/base.privilege-diff.js +104 -22
- package/dist/core/objects/base.privilege.d.ts +1 -0
- package/dist/core/objects/base.privilege.js +9 -2
- package/dist/core/objects/collation/collation.diff.d.ts +2 -3
- package/dist/core/objects/diff-context.d.ts +15 -0
- package/dist/core/objects/diff-context.js +1 -0
- package/dist/core/objects/domain/changes/domain.create.js +4 -2
- package/dist/core/objects/domain/domain.diff.d.ts +2 -8
- package/dist/core/objects/domain/domain.diff.js +16 -77
- package/dist/core/objects/domain/domain.model.js +1 -1
- package/dist/core/objects/event-trigger/event-trigger.diff.d.ts +2 -3
- package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.diff.d.ts +2 -8
- package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.diff.js +13 -77
- package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.js +2 -2
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.diff.d.ts +2 -8
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.diff.js +16 -77
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +1 -1
- package/dist/core/objects/foreign-data-wrapper/server/server.diff.d.ts +2 -8
- package/dist/core/objects/foreign-data-wrapper/server/server.diff.js +13 -77
- package/dist/core/objects/language/language.diff.d.ts +2 -5
- package/dist/core/objects/language/language.diff.js +7 -39
- package/dist/core/objects/materialized-view/materialized-view.diff.d.ts +2 -8
- package/dist/core/objects/materialized-view/materialized-view.diff.js +16 -158
- package/dist/core/objects/materialized-view/materialized-view.model.d.ts +3 -3
- package/dist/core/objects/materialized-view/materialized-view.model.js +1 -1
- package/dist/core/objects/procedure/changes/procedure.alter.js +12 -12
- package/dist/core/objects/procedure/procedure.diff.d.ts +2 -8
- package/dist/core/objects/procedure/procedure.diff.js +16 -77
- package/dist/core/objects/procedure/procedure.model.d.ts +9 -9
- package/dist/core/objects/procedure/procedure.model.js +1 -1
- package/dist/core/objects/publication/changes/publication.alter.d.ts +0 -9
- package/dist/core/objects/publication/changes/publication.alter.js +0 -14
- package/dist/core/objects/publication/changes/publication.types.d.ts +2 -2
- package/dist/core/objects/publication/publication.diff.d.ts +2 -3
- package/dist/core/objects/publication/publication.diff.js +8 -13
- package/dist/core/objects/rls-policy/changes/rls-policy.alter.js +3 -3
- package/dist/core/objects/rls-policy/rls-policy.model.d.ts +2 -2
- package/dist/core/objects/role/role.diff.js +22 -1
- package/dist/core/objects/role/role.model.d.ts +4 -3
- package/dist/core/objects/role/role.model.js +118 -12
- package/dist/core/objects/rule/rule.model.d.ts +1 -1
- package/dist/core/objects/schema/schema.diff.d.ts +2 -8
- package/dist/core/objects/schema/schema.diff.js +16 -77
- package/dist/core/objects/schema/schema.model.js +1 -1
- package/dist/core/objects/sequence/sequence.diff.d.ts +2 -8
- package/dist/core/objects/sequence/sequence.diff.js +16 -79
- package/dist/core/objects/sequence/sequence.model.js +1 -1
- package/dist/core/objects/subscription/subscription.diff.d.ts +2 -3
- package/dist/core/objects/table/changes/table.create.js +3 -0
- package/dist/core/objects/table/table.diff.d.ts +2 -8
- package/dist/core/objects/table/table.diff.js +26 -157
- package/dist/core/objects/table/table.model.d.ts +23 -22
- package/dist/core/objects/table/table.model.js +1 -1
- package/dist/core/objects/trigger/changes/trigger.create.js +2 -4
- package/dist/core/objects/trigger/trigger.model.d.ts +8 -0
- package/dist/core/objects/trigger/trigger.model.js +11 -0
- package/dist/core/objects/type/composite-type/composite-type.diff.d.ts +2 -8
- package/dist/core/objects/type/composite-type/composite-type.diff.js +16 -77
- package/dist/core/objects/type/composite-type/composite-type.model.d.ts +3 -3
- package/dist/core/objects/type/composite-type/composite-type.model.js +2 -1
- package/dist/core/objects/type/enum/enum.diff.d.ts +2 -8
- package/dist/core/objects/type/enum/enum.diff.js +25 -112
- package/dist/core/objects/type/enum/enum.model.js +1 -1
- package/dist/core/objects/type/range/changes/range.create.js +6 -3
- package/dist/core/objects/type/range/range.diff.d.ts +2 -8
- package/dist/core/objects/type/range/range.diff.js +16 -77
- package/dist/core/objects/type/range/range.model.js +1 -1
- package/dist/core/objects/view/view.diff.d.ts +2 -8
- package/dist/core/objects/view/view.diff.js +16 -158
- package/dist/core/objects/view/view.model.d.ts +18 -4
- package/dist/core/objects/view/view.model.js +3 -13
- package/dist/core/plan/apply.js +9 -26
- package/dist/core/plan/create.d.ts +19 -6
- package/dist/core/plan/create.js +134 -174
- package/dist/core/plan/serialize.js +16 -4
- package/dist/core/plan/sql-format/fixtures.js +3 -5
- package/dist/core/plan/sql-format/keyword-case.js +26 -1
- package/dist/core/plan/ssl-config.d.ts +32 -0
- package/dist/core/plan/ssl-config.js +115 -0
- package/dist/core/plan/types.d.ts +6 -0
- package/dist/core/postgres-config.d.ts +14 -0
- package/dist/core/postgres-config.js +53 -2
- package/dist/core/sort/graph-builder.js +10 -0
- package/dist/core/sort/logical-sort.js +31 -23
- package/dist/core/test-utils/assert-valid-sql.d.ts +10 -0
- package/dist/core/test-utils/assert-valid-sql.js +19 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -1
- package/package.json +21 -4
- package/src/cli/app.ts +27 -3
- package/src/cli/bin/cli.ts +6 -0
- package/src/cli/commands/catalog-export.ts +78 -0
- package/src/cli/commands/declarative-apply.diagnostics.test.ts +77 -0
- package/src/cli/commands/declarative-apply.ts +380 -0
- package/src/cli/commands/declarative-export.ts +330 -0
- package/src/cli/commands/plan.ts +28 -7
- package/src/cli/exit-code.test.ts +19 -0
- package/src/cli/exit-code.ts +7 -0
- package/src/cli/formatters/tree/tree.ts +3 -2
- package/src/cli/utils/apply-display.test.ts +348 -0
- package/src/cli/utils/apply-display.ts +238 -0
- package/src/cli/utils/export-display.test.ts +103 -0
- package/src/cli/utils/export-display.ts +275 -0
- package/src/cli/utils/integrations.test.ts +44 -0
- package/src/cli/utils/resolve-input.test.ts +38 -0
- package/src/cli/utils/resolve-input.ts +17 -0
- package/src/core/catalog-export/index.ts +20 -0
- package/src/core/catalog.diff.ts +79 -78
- package/src/core/catalog.model.test.ts +122 -0
- package/src/core/catalog.model.ts +127 -1
- package/src/core/catalog.snapshot.test.ts +464 -0
- package/src/core/catalog.snapshot.ts +289 -0
- package/src/core/declarative-apply/discover-sql.test.ts +103 -0
- package/src/core/declarative-apply/discover-sql.ts +107 -0
- package/src/core/declarative-apply/extract-catalog-providers.ts +220 -0
- package/src/core/declarative-apply/index.test.ts +67 -0
- package/src/core/declarative-apply/index.ts +205 -0
- package/src/core/declarative-apply/round-apply.test.ts +504 -0
- package/src/core/declarative-apply/round-apply.ts +562 -0
- package/src/core/expand-replace-dependencies.test.ts +70 -0
- package/src/core/export/file-mapper.test.ts +816 -0
- package/src/core/export/file-mapper.ts +574 -0
- package/src/core/export/grouper.ts +108 -0
- package/src/core/export/index.ts +129 -0
- package/src/core/export/types.ts +104 -0
- package/src/core/fixtures/empty-catalogs/postgres-15-16-baseline.json +287 -0
- package/src/core/integrations/filter/dsl.test.ts +211 -0
- package/src/core/integrations/filter/dsl.ts +65 -3
- package/src/core/integrations/filter/extractors.test.ts +244 -0
- package/src/core/integrations/filter/extractors.ts +42 -0
- package/src/core/integrations/integration-dsl.ts +10 -0
- package/src/core/integrations/serialize/dsl.test.ts +91 -0
- package/src/core/integrations/supabase.ts +9 -0
- package/src/core/objects/aggregate/aggregate.diff.ts +39 -95
- package/src/core/objects/aggregate/aggregate.model.ts +1 -1
- package/src/core/objects/aggregate/changes/aggregate.alter.test.ts +3 -1
- package/src/core/objects/aggregate/changes/aggregate.comment.test.ts +5 -2
- package/src/core/objects/aggregate/changes/aggregate.create.test.ts +6 -3
- package/src/core/objects/aggregate/changes/aggregate.create.ts +1 -1
- package/src/core/objects/aggregate/changes/aggregate.drop.test.ts +7 -3
- package/src/core/objects/aggregate/changes/aggregate.drop.ts +1 -1
- package/src/core/objects/aggregate/changes/aggregate.privilege.test.ts +9 -3
- package/src/core/objects/base.privilege-diff.ts +178 -30
- package/src/core/objects/base.privilege.ts +9 -2
- package/src/core/objects/collation/changes/collation.alter.test.ts +7 -2
- package/src/core/objects/collation/changes/collation.create.test.ts +7 -2
- package/src/core/objects/collation/changes/collation.drop.test.ts +4 -1
- package/src/core/objects/collation/collation.diff.test.ts +9 -12
- package/src/core/objects/collation/collation.diff.ts +2 -1
- package/src/core/objects/diff-context.ts +16 -0
- package/src/core/objects/domain/changes/domain.alter.test.ts +28 -9
- package/src/core/objects/domain/changes/domain.create.test.ts +32 -2
- package/src/core/objects/domain/changes/domain.create.ts +7 -1
- package/src/core/objects/domain/changes/domain.drop.test.ts +4 -1
- package/src/core/objects/domain/domain.diff.ts +39 -102
- package/src/core/objects/domain/domain.model.ts +1 -1
- package/src/core/objects/event-trigger/changes/event-trigger.alter.test.ts +10 -3
- package/src/core/objects/event-trigger/changes/event-trigger.create.test.ts +4 -1
- package/src/core/objects/event-trigger/changes/event-trigger.drop.test.ts +4 -1
- package/src/core/objects/event-trigger/event-trigger.diff.test.ts +12 -7
- package/src/core/objects/event-trigger/event-trigger.diff.ts +2 -1
- package/src/core/objects/extension/changes/extension.alter.test.ts +7 -2
- package/src/core/objects/extension/changes/extension.create.test.ts +4 -1
- package/src/core/objects/extension/changes/extension.drop.test.ts +4 -1
- package/src/core/objects/extension/extension.model.test.ts +98 -0
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.alter.test.ts +16 -5
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.create.test.ts +51 -16
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.drop.test.ts +4 -1
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.diff.test.ts +111 -4
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.diff.ts +31 -101
- package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.ts +2 -2
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.alter.test.ts +46 -15
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.create.test.ts +13 -4
- package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.drop.test.ts +4 -1
- package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.diff.ts +39 -102
- package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +1 -1
- package/src/core/objects/foreign-data-wrapper/server/changes/server.alter.test.ts +22 -7
- package/src/core/objects/foreign-data-wrapper/server/changes/server.create.test.ts +19 -6
- package/src/core/objects/foreign-data-wrapper/server/changes/server.drop.test.ts +4 -1
- package/src/core/objects/foreign-data-wrapper/server/server.diff.test.ts +95 -0
- package/src/core/objects/foreign-data-wrapper/server/server.diff.ts +31 -101
- package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.alter.test.ts +13 -4
- package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.create.test.ts +16 -5
- package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.drop.test.ts +10 -3
- package/src/core/objects/index/changes/index.alter.test.ts +13 -4
- package/src/core/objects/index/changes/index.create.test.ts +4 -1
- package/src/core/objects/index/changes/index.drop.test.ts +4 -1
- package/src/core/objects/language/changes/language.alter.test.ts +4 -1
- package/src/core/objects/language/changes/language.create.test.ts +4 -1
- package/src/core/objects/language/changes/language.drop.test.ts +4 -1
- package/src/core/objects/language/language.diff.test.ts +86 -4
- package/src/core/objects/language/language.diff.ts +17 -49
- package/src/core/objects/materialized-view/changes/materialized-view.alter.test.ts +10 -3
- package/src/core/objects/materialized-view/changes/materialized-view.create.test.ts +7 -2
- package/src/core/objects/materialized-view/changes/materialized-view.drop.test.ts +4 -1
- package/src/core/objects/materialized-view/materialized-view.diff.test.ts +162 -0
- package/src/core/objects/materialized-view/materialized-view.diff.ts +41 -191
- package/src/core/objects/materialized-view/materialized-view.model.ts +1 -1
- package/src/core/objects/procedure/changes/procedure.alter.test.ts +121 -49
- package/src/core/objects/procedure/changes/procedure.alter.ts +15 -12
- package/src/core/objects/procedure/changes/procedure.create.test.ts +4 -1
- package/src/core/objects/procedure/changes/procedure.drop.test.ts +7 -2
- package/src/core/objects/procedure/procedure.diff.ts +39 -102
- package/src/core/objects/procedure/procedure.model.ts +1 -1
- package/src/core/objects/publication/changes/publication.alter.test.ts +15 -21
- package/src/core/objects/publication/changes/publication.alter.ts +0 -18
- package/src/core/objects/publication/changes/publication.comment.test.ts +5 -2
- package/src/core/objects/publication/changes/publication.create.test.ts +5 -2
- package/src/core/objects/publication/changes/publication.drop.test.ts +3 -1
- package/src/core/objects/publication/changes/publication.types.ts +0 -2
- package/src/core/objects/publication/publication.diff.test.ts +24 -19
- package/src/core/objects/publication/publication.diff.ts +9 -15
- package/src/core/objects/rls-policy/changes/rls-policy.alter.test.ts +31 -14
- package/src/core/objects/rls-policy/changes/rls-policy.alter.ts +3 -3
- package/src/core/objects/rls-policy/changes/rls-policy.create.test.ts +10 -3
- package/src/core/objects/rls-policy/changes/rls-policy.drop.test.ts +4 -1
- package/src/core/objects/role/changes/role.alter.test.ts +31 -15
- package/src/core/objects/role/changes/role.create.test.ts +6 -2
- package/src/core/objects/role/changes/role.drop.test.ts +4 -1
- package/src/core/objects/role/role.diff.test.ts +235 -0
- package/src/core/objects/role/role.diff.ts +21 -1
- package/src/core/objects/role/role.model.ts +122 -14
- package/src/core/objects/rule/changes/rule.alter.test.ts +7 -3
- package/src/core/objects/rule/changes/rule.comment.test.ts +5 -2
- package/src/core/objects/rule/changes/rule.create.test.ts +6 -2
- package/src/core/objects/rule/changes/rule.drop.test.ts +3 -1
- package/src/core/objects/schema/changes/schema.alter.test.ts +4 -1
- package/src/core/objects/schema/changes/schema.create.test.ts +4 -1
- package/src/core/objects/schema/changes/schema.drop.test.ts +4 -1
- package/src/core/objects/schema/schema.diff.ts +39 -102
- package/src/core/objects/schema/schema.model.ts +1 -1
- package/src/core/objects/sequence/changes/sequence.alter.test.ts +11 -5
- package/src/core/objects/sequence/changes/sequence.create.test.ts +8 -3
- package/src/core/objects/sequence/changes/sequence.drop.test.ts +4 -1
- package/src/core/objects/sequence/sequence.diff.test.ts +114 -0
- package/src/core/objects/sequence/sequence.diff.ts +39 -104
- package/src/core/objects/sequence/sequence.model.ts +1 -1
- package/src/core/objects/subscription/changes/subscription.alter.test.ts +15 -5
- package/src/core/objects/subscription/changes/subscription.comment.test.ts +5 -2
- package/src/core/objects/subscription/changes/subscription.create.test.ts +5 -2
- package/src/core/objects/subscription/changes/subscription.drop.test.ts +3 -1
- package/src/core/objects/subscription/subscription.diff.test.ts +16 -11
- package/src/core/objects/subscription/subscription.diff.ts +2 -1
- package/src/core/objects/table/changes/table.alter.test.ts +38 -15
- package/src/core/objects/table/changes/table.create.test.ts +41 -3
- package/src/core/objects/table/changes/table.create.ts +4 -0
- package/src/core/objects/table/changes/table.drop.test.ts +3 -1
- package/src/core/objects/table/table.diff.test.ts +157 -0
- package/src/core/objects/table/table.diff.ts +54 -190
- package/src/core/objects/table/table.model.ts +1 -1
- package/src/core/objects/trigger/changes/trigger.alter.test.ts +8 -4
- package/src/core/objects/trigger/changes/trigger.create.test.ts +5 -1
- package/src/core/objects/trigger/changes/trigger.create.ts +7 -4
- package/src/core/objects/trigger/changes/trigger.drop.test.ts +5 -1
- package/src/core/objects/trigger/trigger.diff.test.ts +1 -0
- package/src/core/objects/trigger/trigger.model.ts +12 -0
- package/src/core/objects/type/composite-type/changes/composite-type.alter.test.ts +10 -4
- package/src/core/objects/type/composite-type/changes/composite-type.create.test.ts +7 -2
- package/src/core/objects/type/composite-type/changes/composite-type.drop.test.ts +4 -1
- package/src/core/objects/type/composite-type/composite-type.diff.test.ts +78 -0
- package/src/core/objects/type/composite-type/composite-type.diff.ts +39 -101
- package/src/core/objects/type/composite-type/composite-type.model.ts +2 -1
- package/src/core/objects/type/enum/changes/enum.alter.test.ts +14 -5
- package/src/core/objects/type/enum/changes/enum.create.test.ts +4 -1
- package/src/core/objects/type/enum/changes/enum.drop.test.ts +4 -1
- package/src/core/objects/type/enum/enum.diff.test.ts +181 -0
- package/src/core/objects/type/enum/enum.diff.ts +58 -146
- package/src/core/objects/type/enum/enum.model.ts +1 -1
- package/src/core/objects/type/range/changes/range.alter.test.ts +3 -1
- package/src/core/objects/type/range/changes/range.create.test.ts +5 -2
- package/src/core/objects/type/range/changes/range.create.ts +6 -2
- package/src/core/objects/type/range/changes/range.drop.test.ts +3 -1
- package/src/core/objects/type/range/range.diff.test.ts +77 -0
- package/src/core/objects/type/range/range.diff.ts +39 -101
- package/src/core/objects/type/range/range.model.ts +1 -1
- package/src/core/objects/view/changes/view.alter.test.ts +8 -3
- package/src/core/objects/view/changes/view.create.test.ts +7 -2
- package/src/core/objects/view/changes/view.drop.test.ts +4 -1
- package/src/core/objects/view/view.diff.test.ts +82 -0
- package/src/core/objects/view/view.diff.ts +41 -191
- package/src/core/objects/view/view.model.ts +3 -17
- package/src/core/plan/apply.ts +9 -27
- package/src/core/plan/create.ts +173 -237
- package/src/core/plan/serialize.test.ts +317 -0
- package/src/core/plan/serialize.ts +18 -4
- package/src/core/plan/sql-format/fixtures.ts +2 -5
- package/src/core/plan/sql-format/format-lowercase-coverage.test.ts +52 -0
- package/src/core/plan/sql-format/format-off.test.ts +14 -17
- package/src/core/plan/sql-format/format-pretty-lower-leading.test.ts +27 -22
- package/src/core/plan/sql-format/format-pretty-narrow.test.ts +17 -21
- package/src/core/plan/sql-format/format-pretty-preserve.test.ts +25 -20
- package/src/core/plan/sql-format/format-pretty-upper.test.ts +23 -20
- package/src/core/plan/sql-format/keyword-case.ts +36 -1
- package/src/core/plan/ssl-config.ts +172 -0
- package/src/core/plan/types.ts +6 -0
- package/src/core/postgres-config.ts +71 -2
- package/src/core/sort/graph-builder.ts +12 -0
- package/src/core/sort/logical-sort.test.ts +371 -0
- package/src/core/sort/logical-sort.ts +32 -25
- package/src/core/sort/topological-sort.test.ts +275 -0
- package/src/core/test-utils/assert-valid-sql.ts +20 -0
- package/src/index.ts +26 -2
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative export command - export a declarative SQL schema from a database diff.
|
|
3
|
+
*/
|
|
4
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { buildCommand } from "@stricli/core";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { exportDeclarativeSchema } from "../../core/export/index.js";
|
|
9
|
+
import { createPlan } from "../../core/plan/index.js";
|
|
10
|
+
import { assertSafePath, buildFileTree, computeFileDiff, formatExportSummary, } from "../utils/export-display.js";
|
|
11
|
+
import { loadIntegrationDSL } from "../utils/integrations.js";
|
|
12
|
+
import { isPostgresUrl, loadCatalogFromFile } from "../utils/resolve-input.js";
|
|
13
|
+
function parseJsonFlag(label, value) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(value);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw new Error(`Invalid ${label} JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export const declarativeExportCommand = buildCommand({
|
|
22
|
+
parameters: {
|
|
23
|
+
flags: {
|
|
24
|
+
source: {
|
|
25
|
+
kind: "parsed",
|
|
26
|
+
brief: "Source (current state): postgres URL or catalog snapshot file path. Omit to export all objects from target.",
|
|
27
|
+
parse: String,
|
|
28
|
+
optional: true,
|
|
29
|
+
},
|
|
30
|
+
target: {
|
|
31
|
+
kind: "parsed",
|
|
32
|
+
brief: "Target (desired state): postgres URL or catalog snapshot file path",
|
|
33
|
+
parse: String,
|
|
34
|
+
},
|
|
35
|
+
output: {
|
|
36
|
+
kind: "parsed",
|
|
37
|
+
brief: "Output directory path for declarative schema files",
|
|
38
|
+
parse: String,
|
|
39
|
+
},
|
|
40
|
+
integration: {
|
|
41
|
+
kind: "parsed",
|
|
42
|
+
brief: "Integration name (e.g., 'supabase') or path to integration JSON file",
|
|
43
|
+
parse: String,
|
|
44
|
+
optional: true,
|
|
45
|
+
},
|
|
46
|
+
filter: {
|
|
47
|
+
kind: "parsed",
|
|
48
|
+
brief: 'Filter DSL as inline JSON (e.g., \'{"schema":"public"}\')',
|
|
49
|
+
parse: (v) => parseJsonFlag("filter", v),
|
|
50
|
+
optional: true,
|
|
51
|
+
},
|
|
52
|
+
serialize: {
|
|
53
|
+
kind: "parsed",
|
|
54
|
+
brief: 'Serialize DSL as inline JSON array (e.g., \'[{"when":{"type":"schema"},"options":{"skipAuthorization":true}}]\')',
|
|
55
|
+
parse: (v) => parseJsonFlag("serialize", v),
|
|
56
|
+
optional: true,
|
|
57
|
+
},
|
|
58
|
+
"grouping-mode": {
|
|
59
|
+
kind: "enum",
|
|
60
|
+
brief: "How grouped entities are organized on disk",
|
|
61
|
+
values: ["single-file", "subdirectory"],
|
|
62
|
+
optional: true,
|
|
63
|
+
},
|
|
64
|
+
"group-patterns": {
|
|
65
|
+
kind: "parsed",
|
|
66
|
+
brief: 'JSON array of {pattern, name} objects (e.g., \'[{"pattern":"^auth","name":"auth"}]\')',
|
|
67
|
+
parse: (v) => {
|
|
68
|
+
const parsed = parseJsonFlag("group-patterns", v);
|
|
69
|
+
if (!Array.isArray(parsed)) {
|
|
70
|
+
throw new Error("group-patterns must be a JSON array");
|
|
71
|
+
}
|
|
72
|
+
return parsed;
|
|
73
|
+
},
|
|
74
|
+
optional: true,
|
|
75
|
+
},
|
|
76
|
+
"flat-schemas": {
|
|
77
|
+
kind: "parsed",
|
|
78
|
+
brief: "Comma-separated list of schemas to flatten (e.g., partman,pgboss,audit)",
|
|
79
|
+
parse: String,
|
|
80
|
+
optional: true,
|
|
81
|
+
},
|
|
82
|
+
"format-options": {
|
|
83
|
+
kind: "parsed",
|
|
84
|
+
brief: 'SQL format options as inline JSON (e.g., \'{"keywordCase":"lower","maxWidth":180}\')',
|
|
85
|
+
parse: (v) => parseJsonFlag("format-options", v),
|
|
86
|
+
optional: true,
|
|
87
|
+
},
|
|
88
|
+
force: {
|
|
89
|
+
kind: "boolean",
|
|
90
|
+
brief: "Remove entire output directory before writing",
|
|
91
|
+
optional: true,
|
|
92
|
+
},
|
|
93
|
+
"dry-run": {
|
|
94
|
+
kind: "boolean",
|
|
95
|
+
brief: "Show tree and summary without writing files",
|
|
96
|
+
optional: true,
|
|
97
|
+
},
|
|
98
|
+
"diff-focus": {
|
|
99
|
+
kind: "boolean",
|
|
100
|
+
brief: "Show only files that changed (created/updated/deleted) in the tree",
|
|
101
|
+
optional: true,
|
|
102
|
+
},
|
|
103
|
+
verbose: {
|
|
104
|
+
kind: "boolean",
|
|
105
|
+
brief: "Show detailed output",
|
|
106
|
+
optional: true,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
aliases: {
|
|
110
|
+
s: "source",
|
|
111
|
+
t: "target",
|
|
112
|
+
o: "output",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
docs: {
|
|
116
|
+
brief: "Export a declarative schema from a database diff",
|
|
117
|
+
fullDescription: `
|
|
118
|
+
Export a declarative SQL schema by comparing two databases (source → target).
|
|
119
|
+
Writes .sql files to the output directory, grouped by object type and optional
|
|
120
|
+
grouping rules.
|
|
121
|
+
|
|
122
|
+
When --source is omitted, all objects from the target database are exported
|
|
123
|
+
(equivalent to diffing from an empty database).
|
|
124
|
+
|
|
125
|
+
Flags:
|
|
126
|
+
source - Source database connection URL (optional; omit for full export)
|
|
127
|
+
target - Target database connection URL (desired state)
|
|
128
|
+
output - Directory path for generated .sql files
|
|
129
|
+
integration - Integration name or path (e.g., supabase) for filter/serialize
|
|
130
|
+
filter - Filter DSL as JSON to include/exclude changes
|
|
131
|
+
serialize - Serialize DSL as JSON array for custom SQL generation
|
|
132
|
+
grouping-mode - single-file or subdirectory for grouped entities
|
|
133
|
+
group-patterns - JSON array of {pattern, name} for name-based grouping
|
|
134
|
+
flat-schemas - Comma-separated schemas to merge into one file per category
|
|
135
|
+
format-options - SQL format options as JSON
|
|
136
|
+
force - Remove output directory before writing (full replace)
|
|
137
|
+
dry-run - Show tree and summary only, do not write files
|
|
138
|
+
diff-focus - Show only changed files (created/updated/deleted) in the tree
|
|
139
|
+
verbose - Show detailed output
|
|
140
|
+
|
|
141
|
+
After export, a tip is printed with the command to apply the schema to an empty database.
|
|
142
|
+
`.trim(),
|
|
143
|
+
},
|
|
144
|
+
async func(flags) {
|
|
145
|
+
const { compileSerializeDSL } = await import("../../core/integrations/serialize/dsl.js");
|
|
146
|
+
let filterOption = flags.filter;
|
|
147
|
+
let serializeOption = flags.serialize;
|
|
148
|
+
let integrationEmptyCatalog;
|
|
149
|
+
if (flags.integration) {
|
|
150
|
+
const integrationDSL = await loadIntegrationDSL(flags.integration);
|
|
151
|
+
filterOption = filterOption ?? integrationDSL.filter;
|
|
152
|
+
serializeOption = serializeOption ?? integrationDSL.serialize;
|
|
153
|
+
integrationEmptyCatalog = integrationDSL.emptyCatalog;
|
|
154
|
+
}
|
|
155
|
+
const resolvedSource = flags.source
|
|
156
|
+
? isPostgresUrl(flags.source)
|
|
157
|
+
? flags.source
|
|
158
|
+
: await loadCatalogFromFile(flags.source)
|
|
159
|
+
: integrationEmptyCatalog
|
|
160
|
+
? (await import("../../core/catalog.snapshot.js")).deserializeCatalog(integrationEmptyCatalog)
|
|
161
|
+
: null;
|
|
162
|
+
const resolvedTarget = isPostgresUrl(flags.target)
|
|
163
|
+
? flags.target
|
|
164
|
+
: await loadCatalogFromFile(flags.target);
|
|
165
|
+
// Pass raw DSL to createPlan (not pre-compiled functions).
|
|
166
|
+
// createPlan compiles them internally and uses the DSL type to correctly
|
|
167
|
+
// determine cascade behavior: DSL filters disable cascading by default
|
|
168
|
+
// (unless cascade:true is set), preventing unintended exclusion of
|
|
169
|
+
// changes that depend on filtered objects (e.g. RLS policies that
|
|
170
|
+
// reference auth.uid() when the auth schema is filtered out).
|
|
171
|
+
const planResult = await createPlan(resolvedSource, resolvedTarget, {
|
|
172
|
+
filter: filterOption,
|
|
173
|
+
serialize: serializeOption,
|
|
174
|
+
skipDefaultPrivilegeSubtraction: true,
|
|
175
|
+
});
|
|
176
|
+
if (!planResult) {
|
|
177
|
+
this.process.stdout.write("No changes detected.\n");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const hasGrouping = flags["grouping-mode"] !== undefined ||
|
|
181
|
+
(flags["group-patterns"] !== undefined &&
|
|
182
|
+
flags["group-patterns"].length > 0) ||
|
|
183
|
+
(flags["flat-schemas"] !== undefined && flags["flat-schemas"].length > 0);
|
|
184
|
+
let grouping;
|
|
185
|
+
if (hasGrouping) {
|
|
186
|
+
grouping = {
|
|
187
|
+
mode: flags["grouping-mode"] ?? "single-file",
|
|
188
|
+
groupPatterns: flags["group-patterns"],
|
|
189
|
+
autoGroupPartitions: true,
|
|
190
|
+
flatSchemas: flags["flat-schemas"] !== undefined
|
|
191
|
+
? flags["flat-schemas"]
|
|
192
|
+
.split(",")
|
|
193
|
+
.map((s) => s.trim())
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
: undefined,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const serializeFn = serializeOption !== undefined
|
|
199
|
+
? compileSerializeDSL(serializeOption)
|
|
200
|
+
: undefined;
|
|
201
|
+
const output = exportDeclarativeSchema(planResult, {
|
|
202
|
+
integration: serializeFn !== undefined ? { serialize: serializeFn } : undefined,
|
|
203
|
+
formatOptions: flags["format-options"] ?? undefined,
|
|
204
|
+
grouping,
|
|
205
|
+
onWarning: (msg) => {
|
|
206
|
+
this.process.stderr.write(chalk.yellow(`Warning: ${msg}\n`));
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
const outputDir = path.resolve(flags.output);
|
|
210
|
+
const applyTip = (dir) => `\nTip: To apply this schema to an empty database, run:\n pgdelta declarative apply --path ${dir} --target <database_url>\n`;
|
|
211
|
+
const diff = await computeFileDiff(outputDir, output.files);
|
|
212
|
+
this.process.stdout.write("\n");
|
|
213
|
+
this.process.stdout.write(`${buildFileTree(output.files.map((f) => f.path), path.basename(outputDir) || outputDir, { diff, diffFocus: !!flags["diff-focus"] })}\n`);
|
|
214
|
+
this.process.stdout.write("\n");
|
|
215
|
+
this.process.stdout.write(`${chalk.green("+")} created ${chalk.yellow("~")} updated ${chalk.red("-")} deleted\n`);
|
|
216
|
+
this.process.stdout.write("\n");
|
|
217
|
+
const summary = formatExportSummary(diff, !!flags["dry-run"]);
|
|
218
|
+
if (summary) {
|
|
219
|
+
this.process.stdout.write(`${summary}\n`);
|
|
220
|
+
}
|
|
221
|
+
const totalChanges = planResult.sortedChanges.length;
|
|
222
|
+
const totalStatements = output.files.reduce((s, f) => s + f.statements, 0);
|
|
223
|
+
this.process.stdout.write(`Changes: ${totalChanges} | Files: ${output.files.length} | Statements: ${totalStatements}\n`);
|
|
224
|
+
if (flags["dry-run"]) {
|
|
225
|
+
this.process.stdout.write(chalk.dim("\n(dry-run: no files written)\n"));
|
|
226
|
+
this.process.stdout.write(chalk.cyan(applyTip(outputDir)));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (flags.force) {
|
|
230
|
+
await rm(outputDir, { recursive: true, force: true });
|
|
231
|
+
await mkdir(outputDir, { recursive: true });
|
|
232
|
+
}
|
|
233
|
+
else if (diff.deleted.length > 0) {
|
|
234
|
+
this.process.stderr.write(chalk.yellow(`Warning: ${diff.deleted.length} existing file(s) will no longer be present. Use --force to replace the output directory.\n`));
|
|
235
|
+
}
|
|
236
|
+
for (const file of output.files) {
|
|
237
|
+
assertSafePath(file.path, outputDir);
|
|
238
|
+
const filePath = path.join(outputDir, file.path);
|
|
239
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
240
|
+
await writeFile(filePath, file.sql);
|
|
241
|
+
}
|
|
242
|
+
this.process.stdout.write(chalk.green(`Wrote ${output.files.length} file(s) to ${outputDir}\n`));
|
|
243
|
+
this.process.stdout.write(chalk.cyan(applyTip(outputDir)));
|
|
244
|
+
},
|
|
245
|
+
});
|
|
@@ -4,19 +4,22 @@
|
|
|
4
4
|
import { writeFile } from "node:fs/promises";
|
|
5
5
|
import { buildCommand } from "@stricli/core";
|
|
6
6
|
import { createPlan } from "../../core/plan/index.js";
|
|
7
|
+
import { setCommandExitCode } from "../exit-code.js";
|
|
7
8
|
import { loadIntegrationDSL } from "../utils/integrations.js";
|
|
9
|
+
import { isPostgresUrl, loadCatalogFromFile } from "../utils/resolve-input.js";
|
|
8
10
|
import { formatPlanForDisplay } from "../utils.js";
|
|
9
11
|
export const planCommand = buildCommand({
|
|
10
12
|
parameters: {
|
|
11
13
|
flags: {
|
|
12
14
|
source: {
|
|
13
15
|
kind: "parsed",
|
|
14
|
-
brief: "Source
|
|
16
|
+
brief: "Source (current state): postgres URL or catalog snapshot file path. Omit for empty baseline.",
|
|
15
17
|
parse: String,
|
|
18
|
+
optional: true,
|
|
16
19
|
},
|
|
17
20
|
target: {
|
|
18
21
|
kind: "parsed",
|
|
19
|
-
brief: "Target
|
|
22
|
+
brief: "Target (desired state): postgres URL or catalog snapshot file path",
|
|
20
23
|
parse: String,
|
|
21
24
|
},
|
|
22
25
|
format: {
|
|
@@ -103,16 +106,26 @@ json/sql outputs are available for artifacts or piping.
|
|
|
103
106
|
`.trim(),
|
|
104
107
|
},
|
|
105
108
|
async func(flags) {
|
|
106
|
-
// Load integration if provided and extract filter/serialize DSL
|
|
107
109
|
let filterOption = flags.filter;
|
|
108
110
|
let serializeOption = flags.serialize;
|
|
111
|
+
let integrationEmptyCatalog;
|
|
109
112
|
if (flags.integration) {
|
|
110
113
|
const integrationDSL = await loadIntegrationDSL(flags.integration);
|
|
111
|
-
// Use integration DSL if explicit flags not provided
|
|
112
114
|
filterOption = filterOption ?? integrationDSL.filter;
|
|
113
115
|
serializeOption = serializeOption ?? integrationDSL.serialize;
|
|
116
|
+
integrationEmptyCatalog = integrationDSL.emptyCatalog;
|
|
114
117
|
}
|
|
115
|
-
const
|
|
118
|
+
const resolvedSource = flags.source
|
|
119
|
+
? isPostgresUrl(flags.source)
|
|
120
|
+
? flags.source
|
|
121
|
+
: await loadCatalogFromFile(flags.source)
|
|
122
|
+
: integrationEmptyCatalog
|
|
123
|
+
? (await import("../../core/catalog.snapshot.js")).deserializeCatalog(integrationEmptyCatalog)
|
|
124
|
+
: null;
|
|
125
|
+
const resolvedTarget = isPostgresUrl(flags.target)
|
|
126
|
+
? flags.target
|
|
127
|
+
: await loadCatalogFromFile(flags.target);
|
|
128
|
+
const planResult = await createPlan(resolvedSource, resolvedTarget, {
|
|
116
129
|
role: flags.role,
|
|
117
130
|
filter: filterOption,
|
|
118
131
|
serialize: serializeOption,
|
|
@@ -153,6 +166,6 @@ json/sql outputs are available for artifacts or piping.
|
|
|
153
166
|
}
|
|
154
167
|
}
|
|
155
168
|
// Exit code 2 indicates changes were detected
|
|
156
|
-
|
|
169
|
+
setCommandExitCode(2);
|
|
157
170
|
},
|
|
158
171
|
});
|
|
@@ -12,7 +12,7 @@ export function formatTree(plan) {
|
|
|
12
12
|
// Summary
|
|
13
13
|
const total = countTotalChanges(plan);
|
|
14
14
|
lines.push(chalk.bold(`📋 Migration Plan: ${total} change${total !== 1 ? "s" : ""}`));
|
|
15
|
-
const summary =
|
|
15
|
+
const summary = buildPlanSummaryTable(plan);
|
|
16
16
|
if (summary) {
|
|
17
17
|
lines.push("");
|
|
18
18
|
lines.push(summary);
|
|
@@ -38,8 +38,9 @@ function countTotalChanges(plan) {
|
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Build summary as a table showing counts by entity type and operation.
|
|
41
|
+
* Exported for use by declarative-export to show the same summary style.
|
|
41
42
|
*/
|
|
42
|
-
function
|
|
43
|
+
function buildPlanSummaryTable(plan) {
|
|
43
44
|
// Count by object type
|
|
44
45
|
const byType = {};
|
|
45
46
|
countFromHierarchy(plan, byType);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display utilities for the declarative-apply command.
|
|
3
|
+
*
|
|
4
|
+
* Pure formatting and location-resolution functions — no CLI framework dependency.
|
|
5
|
+
* Used to:
|
|
6
|
+
* - Map pg-topo diagnostics into display items (optionally grouped by message/code).
|
|
7
|
+
* - Resolve statement IDs to file paths and line/column for error output.
|
|
8
|
+
* - Format StatementErrors in a pgAdmin-style multi-line block.
|
|
9
|
+
*/
|
|
10
|
+
import type { Diagnostic } from "@supabase/pg-topo";
|
|
11
|
+
import type { StatementError } from "../../core/declarative-apply/round-apply.ts";
|
|
12
|
+
/**
|
|
13
|
+
* Convert a 1-based character offset in a string to 1-based line and column.
|
|
14
|
+
* Used when mapping PostgreSQL error positions (in SQL) to file locations.
|
|
15
|
+
*/
|
|
16
|
+
export declare function positionToLineColumn(sql: string, position: number): {
|
|
17
|
+
line: number;
|
|
18
|
+
column: number;
|
|
19
|
+
};
|
|
20
|
+
/** Input to buildDiagnosticDisplayItems: a pg-topo diagnostic plus optional location and object key. */
|
|
21
|
+
export type DiagnosticDisplayEntry = {
|
|
22
|
+
diagnostic: Diagnostic;
|
|
23
|
+
location?: string;
|
|
24
|
+
requiredObjectKey?: string;
|
|
25
|
+
};
|
|
26
|
+
/** One display row for a diagnostic (or a group of same-code diagnostics with multiple locations). */
|
|
27
|
+
type DiagnosticDisplayItem = {
|
|
28
|
+
code: string;
|
|
29
|
+
message: string;
|
|
30
|
+
suggestedFix?: string;
|
|
31
|
+
requiredObjectKey?: string;
|
|
32
|
+
locations: string[];
|
|
33
|
+
};
|
|
34
|
+
/** Extract requiredObjectKey from a pg-topo diagnostic if present and non-empty. */
|
|
35
|
+
export declare const requiredObjectKeyFromDiagnostic: (diagnostic: Diagnostic) => string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Turn diagnostic entries into display items. If grouped is true, entries with
|
|
38
|
+
* the same code/message/suggestedFix are merged into one item with multiple locations.
|
|
39
|
+
*/
|
|
40
|
+
export declare const buildDiagnosticDisplayItems: (entries: DiagnosticDisplayEntry[], grouped: boolean) => DiagnosticDisplayItem[];
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the full path to a .sql file from the schema path (directory or single file)
|
|
43
|
+
* and a relative file path (e.g. from a statement id). If schemaPath is a file, its
|
|
44
|
+
* directory is used as the base.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveSqlFilePath(schemaPath: string, relativeFilePath: string): Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Format a StatementError in pgAdmin-style: ERROR, Detail, SQL state, optional
|
|
49
|
+
* Context, Hint, and Location (resolving the .sql file and line/column when possible).
|
|
50
|
+
*/
|
|
51
|
+
export declare function formatStatementError(err: StatementError, schemaPath: string): Promise<string>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display utilities for the declarative-apply command.
|
|
3
|
+
*
|
|
4
|
+
* Pure formatting and location-resolution functions — no CLI framework dependency.
|
|
5
|
+
* Used to:
|
|
6
|
+
* - Map pg-topo diagnostics into display items (optionally grouped by message/code).
|
|
7
|
+
* - Resolve statement IDs to file paths and line/column for error output.
|
|
8
|
+
* - Format StatementErrors in a pgAdmin-style multi-line block.
|
|
9
|
+
*/
|
|
10
|
+
import { readFile, stat } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
/**
|
|
13
|
+
* Convert a 1-based character offset in a string to 1-based line and column.
|
|
14
|
+
* Used when mapping PostgreSQL error positions (in SQL) to file locations.
|
|
15
|
+
*/
|
|
16
|
+
export function positionToLineColumn(sql, position) {
|
|
17
|
+
const lines = sql.split("\n");
|
|
18
|
+
let offset = 0;
|
|
19
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20
|
+
const lineLen = lines[i].length + (i < lines.length - 1 ? 1 : 0);
|
|
21
|
+
if (position <= offset + lineLen) {
|
|
22
|
+
return { line: i + 1, column: position - offset };
|
|
23
|
+
}
|
|
24
|
+
offset += lineLen;
|
|
25
|
+
}
|
|
26
|
+
const last = lines.length;
|
|
27
|
+
const lastLineLen = lines[last - 1]?.length ?? 0;
|
|
28
|
+
return { line: last, column: lastLineLen + 1 };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse a statement id in the form "filePath:statementIndex" into components.
|
|
32
|
+
* The last colon separates path from index (paths may contain colons).
|
|
33
|
+
* Returns null if the format is invalid.
|
|
34
|
+
*/
|
|
35
|
+
function parseStatementId(id) {
|
|
36
|
+
const lastColon = id.lastIndexOf(":");
|
|
37
|
+
if (lastColon === -1)
|
|
38
|
+
return null;
|
|
39
|
+
const filePath = id.slice(0, lastColon);
|
|
40
|
+
const n = Number.parseInt(id.slice(lastColon + 1), 10);
|
|
41
|
+
if (!Number.isInteger(n) || n < 0)
|
|
42
|
+
return null;
|
|
43
|
+
return { filePath, statementIndex: n };
|
|
44
|
+
}
|
|
45
|
+
/** Extract requiredObjectKey from a pg-topo diagnostic if present and non-empty. */
|
|
46
|
+
export const requiredObjectKeyFromDiagnostic = (diagnostic) => {
|
|
47
|
+
const value = diagnostic.details?.requiredObjectKey;
|
|
48
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
49
|
+
};
|
|
50
|
+
/** Build a stable key for grouping diagnostics with the same code, message, and suggested fix. */
|
|
51
|
+
const diagnosticDisplayGroupKey = (entry) => [
|
|
52
|
+
entry.diagnostic.code,
|
|
53
|
+
entry.diagnostic.message,
|
|
54
|
+
entry.diagnostic.suggestedFix ?? "",
|
|
55
|
+
entry.requiredObjectKey ?? "",
|
|
56
|
+
].join("\u0000");
|
|
57
|
+
/**
|
|
58
|
+
* Turn diagnostic entries into display items. If grouped is true, entries with
|
|
59
|
+
* the same code/message/suggestedFix are merged into one item with multiple locations.
|
|
60
|
+
*/
|
|
61
|
+
export const buildDiagnosticDisplayItems = (entries, grouped) => {
|
|
62
|
+
if (!grouped) {
|
|
63
|
+
return entries.map((entry) => ({
|
|
64
|
+
code: entry.diagnostic.code,
|
|
65
|
+
message: entry.diagnostic.message,
|
|
66
|
+
suggestedFix: entry.diagnostic.suggestedFix,
|
|
67
|
+
requiredObjectKey: entry.requiredObjectKey,
|
|
68
|
+
locations: entry.location ? [entry.location] : [],
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
const groupedItems = new Map();
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const key = diagnosticDisplayGroupKey(entry);
|
|
74
|
+
const existing = groupedItems.get(key);
|
|
75
|
+
if (!existing) {
|
|
76
|
+
groupedItems.set(key, {
|
|
77
|
+
code: entry.diagnostic.code,
|
|
78
|
+
message: entry.diagnostic.message,
|
|
79
|
+
suggestedFix: entry.diagnostic.suggestedFix,
|
|
80
|
+
requiredObjectKey: entry.requiredObjectKey,
|
|
81
|
+
locations: entry.location ? [entry.location] : [],
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (entry.location && !existing.locations.includes(entry.location)) {
|
|
86
|
+
existing.locations.push(entry.location);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return [...groupedItems.values()];
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the full path to a .sql file from the schema path (directory or single file)
|
|
93
|
+
* and a relative file path (e.g. from a statement id). If schemaPath is a file, its
|
|
94
|
+
* directory is used as the base.
|
|
95
|
+
*/
|
|
96
|
+
export async function resolveSqlFilePath(schemaPath, relativeFilePath) {
|
|
97
|
+
try {
|
|
98
|
+
const statResult = await stat(schemaPath);
|
|
99
|
+
const baseDir = statResult.isFile() ? path.dirname(schemaPath) : schemaPath;
|
|
100
|
+
return path.join(baseDir, relativeFilePath);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return path.join(schemaPath, relativeFilePath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Find the 0-based start offset of statementSql in fileContent.
|
|
108
|
+
* Tries exact match first, then trimmed match. Returns -1 if not found.
|
|
109
|
+
*/
|
|
110
|
+
function findStatementStartInFile(fileContent, statementSql) {
|
|
111
|
+
const exact = fileContent.indexOf(statementSql);
|
|
112
|
+
if (exact !== -1)
|
|
113
|
+
return exact;
|
|
114
|
+
const trimmedStmt = statementSql.trim();
|
|
115
|
+
if (!trimmedStmt)
|
|
116
|
+
return -1;
|
|
117
|
+
const trimmed = fileContent.indexOf(trimmedStmt);
|
|
118
|
+
if (trimmed !== -1)
|
|
119
|
+
return trimmed;
|
|
120
|
+
return -1;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Format a StatementError in pgAdmin-style: ERROR, Detail, SQL state, optional
|
|
124
|
+
* Context, Hint, and Location (resolving the .sql file and line/column when possible).
|
|
125
|
+
*/
|
|
126
|
+
export async function formatStatementError(err, schemaPath) {
|
|
127
|
+
const lines = [];
|
|
128
|
+
lines.push(`ERROR: ${err.message}`);
|
|
129
|
+
if (err.detail) {
|
|
130
|
+
lines.push(`Detail: ${err.detail}`);
|
|
131
|
+
}
|
|
132
|
+
lines.push(`SQL state: ${err.code}`);
|
|
133
|
+
if (err.position !== undefined && err.statement.sql.length > 0) {
|
|
134
|
+
lines.push(`Character: ${err.position}`);
|
|
135
|
+
const pos = Math.max(0, Math.min(err.position - 1, err.statement.sql.length));
|
|
136
|
+
const contextStart = Math.max(0, pos - 40);
|
|
137
|
+
const contextEnd = Math.min(err.statement.sql.length, pos + 40);
|
|
138
|
+
const snippet = err.statement.sql.slice(contextStart, contextEnd);
|
|
139
|
+
const oneLine = snippet.replace(/\s+/g, " ").trim();
|
|
140
|
+
lines.push(`Context: ${oneLine || "(empty)"}`);
|
|
141
|
+
}
|
|
142
|
+
if (err.hint) {
|
|
143
|
+
lines.push(`Hint: ${err.hint}`);
|
|
144
|
+
}
|
|
145
|
+
const parsed = parseStatementId(err.statement.id);
|
|
146
|
+
if (parsed) {
|
|
147
|
+
let locationLine;
|
|
148
|
+
try {
|
|
149
|
+
const fullPath = await resolveSqlFilePath(schemaPath, parsed.filePath);
|
|
150
|
+
const fileContent = await readFile(fullPath, "utf-8");
|
|
151
|
+
const statementStart = findStatementStartInFile(fileContent, err.statement.sql);
|
|
152
|
+
if (statementStart !== -1) {
|
|
153
|
+
if (err.position !== undefined && err.statement.sql.length > 0) {
|
|
154
|
+
const fileErrorOffset = statementStart + (err.position - 1);
|
|
155
|
+
const fileErrorPosition = Math.min(fileErrorOffset + 1, fileContent.length);
|
|
156
|
+
const { line, column } = positionToLineColumn(fileContent, Math.max(1, fileErrorPosition));
|
|
157
|
+
locationLine = `Location: ${parsed.filePath}:${line}:${column}`;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const { line } = positionToLineColumn(fileContent, statementStart + 1);
|
|
161
|
+
locationLine = `Location: ${parsed.filePath}:${line}`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
locationLine = `Location: ${parsed.filePath} (statement ${parsed.statementIndex})`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
if (err.position !== undefined && err.statement.sql.length > 0) {
|
|
170
|
+
const { line, column } = positionToLineColumn(err.statement.sql, err.position);
|
|
171
|
+
locationLine = `Location: ${parsed.filePath} (statement ${parsed.statementIndex}, line ${line}, column ${column})`;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
locationLine = `Location: ${parsed.filePath} (statement ${parsed.statementIndex})`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
lines.push(locationLine);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
lines.push(`Location: ${err.statement.id}`);
|
|
181
|
+
}
|
|
182
|
+
return lines.map((l) => ` ${l}`).join("\n");
|
|
183
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI helpers for declarative export: file tree, diff, and summary formatting.
|
|
3
|
+
*/
|
|
4
|
+
import type { FileEntry } from "../../core/export/types.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Ensure a relative file path does not escape the output directory.
|
|
7
|
+
* Uses Node.js path.resolve + startsWith as the canonical traversal check.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertSafePath(filePath: string, outputDir: string): void;
|
|
10
|
+
interface FileDiffResult {
|
|
11
|
+
created: string[];
|
|
12
|
+
updated: string[];
|
|
13
|
+
deleted: string[];
|
|
14
|
+
unchanged: string[];
|
|
15
|
+
}
|
|
16
|
+
interface BuildFileTreeOptions {
|
|
17
|
+
/** When provided, leaf paths are prefixed with + / ~ / - and colorized (created / updated / deleted). */
|
|
18
|
+
diff?: FileDiffResult;
|
|
19
|
+
/** When true, only paths that are created, updated, or deleted are shown; unchanged are omitted. Includes diff.deleted in the tree. */
|
|
20
|
+
diffFocus?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a directory tree string from file paths.
|
|
24
|
+
* Groups by directory, shows files as leaves with indentation.
|
|
25
|
+
* When options.diff is provided, leaf names are prefixed with + (created), ~ (updated), - (deleted) and colorized.
|
|
26
|
+
* When options.diffFocus is true, only changed paths (and their ancestors) are shown; unchanged files are omitted.
|
|
27
|
+
*
|
|
28
|
+
* @param files - Array of relative file paths (e.g. ["schemas/public/schema.sql", "schemas/public/tables/users.sql"])
|
|
29
|
+
* @param outputDir - Display name for the root (e.g. "declarative-schemas")
|
|
30
|
+
* @param options - Optional diff and diffFocus for symbols and filtering
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildFileTree(files: string[], outputDir: string, options?: BuildFileTreeOptions): string;
|
|
33
|
+
/**
|
|
34
|
+
* Compare existing output directory with new file set.
|
|
35
|
+
* Returns created, updated, deleted, and unchanged paths.
|
|
36
|
+
*/
|
|
37
|
+
export declare function computeFileDiff(outputDir: string, newFiles: FileEntry[]): Promise<FileDiffResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Format the created/deleted/updated summary with colors.
|
|
40
|
+
* In dry-run mode, uses "would create/delete/update" phrasing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function formatExportSummary(diff: FileDiffResult, dryRun: boolean): string;
|
|
43
|
+
export {};
|