@supabase/pg-delta 1.0.0-alpha.4 → 1.0.0-alpha.6
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,348 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { Diagnostic, DiagnosticCode } from "@supabase/pg-topo";
|
|
6
|
+
import type { StatementError } from "../../core/declarative-apply/round-apply.ts";
|
|
7
|
+
import {
|
|
8
|
+
buildDiagnosticDisplayItems,
|
|
9
|
+
type DiagnosticDisplayEntry,
|
|
10
|
+
formatStatementError,
|
|
11
|
+
positionToLineColumn,
|
|
12
|
+
requiredObjectKeyFromDiagnostic,
|
|
13
|
+
resolveSqlFilePath,
|
|
14
|
+
} from "./apply-display.ts";
|
|
15
|
+
|
|
16
|
+
describe("positionToLineColumn", () => {
|
|
17
|
+
test("single line, position at start", () => {
|
|
18
|
+
expect(positionToLineColumn("hello", 1)).toEqual({ line: 1, column: 1 });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("single line, position in the middle", () => {
|
|
22
|
+
expect(positionToLineColumn("hello", 3)).toEqual({ line: 1, column: 3 });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("single line, position at end", () => {
|
|
26
|
+
expect(positionToLineColumn("hello", 5)).toEqual({ line: 1, column: 5 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("multi-line, first line", () => {
|
|
30
|
+
expect(positionToLineColumn("ab\ncd\nef", 2)).toEqual({
|
|
31
|
+
line: 1,
|
|
32
|
+
column: 2,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("multi-line, second line start", () => {
|
|
37
|
+
expect(positionToLineColumn("ab\ncd\nef", 4)).toEqual({
|
|
38
|
+
line: 2,
|
|
39
|
+
column: 1,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("multi-line, third line", () => {
|
|
44
|
+
expect(positionToLineColumn("ab\ncd\nef", 7)).toEqual({
|
|
45
|
+
line: 3,
|
|
46
|
+
column: 1,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("position past end falls back to last line", () => {
|
|
51
|
+
expect(positionToLineColumn("ab\ncd", 100)).toEqual({
|
|
52
|
+
line: 2,
|
|
53
|
+
column: 3,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("empty string", () => {
|
|
58
|
+
expect(positionToLineColumn("", 1)).toEqual({ line: 1, column: 1 });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("requiredObjectKeyFromDiagnostic", () => {
|
|
63
|
+
test("returns value when present and non-empty", () => {
|
|
64
|
+
const diag: Diagnostic = {
|
|
65
|
+
code: "UNRESOLVED_DEPENDENCY",
|
|
66
|
+
message: "warning",
|
|
67
|
+
details: { requiredObjectKey: "public.users" },
|
|
68
|
+
};
|
|
69
|
+
expect(requiredObjectKeyFromDiagnostic(diag)).toBe("public.users");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("returns undefined for empty string", () => {
|
|
73
|
+
const diag: Diagnostic = {
|
|
74
|
+
code: "UNRESOLVED_DEPENDENCY",
|
|
75
|
+
message: "warning",
|
|
76
|
+
details: { requiredObjectKey: "" },
|
|
77
|
+
};
|
|
78
|
+
expect(requiredObjectKeyFromDiagnostic(diag)).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("returns undefined when details is undefined", () => {
|
|
82
|
+
const diag: Diagnostic = {
|
|
83
|
+
code: "UNRESOLVED_DEPENDENCY",
|
|
84
|
+
message: "warning",
|
|
85
|
+
};
|
|
86
|
+
expect(requiredObjectKeyFromDiagnostic(diag)).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("returns undefined for non-string value", () => {
|
|
90
|
+
const diag: Diagnostic = {
|
|
91
|
+
code: "UNRESOLVED_DEPENDENCY",
|
|
92
|
+
message: "warning",
|
|
93
|
+
details: { requiredObjectKey: 42 },
|
|
94
|
+
};
|
|
95
|
+
expect(requiredObjectKeyFromDiagnostic(diag)).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("buildDiagnosticDisplayItems", () => {
|
|
100
|
+
const makeDiag = (
|
|
101
|
+
code: DiagnosticCode,
|
|
102
|
+
message: string,
|
|
103
|
+
suggestedFix?: string,
|
|
104
|
+
): Diagnostic => ({ code, message, suggestedFix });
|
|
105
|
+
|
|
106
|
+
test("ungrouped mode returns one item per entry", () => {
|
|
107
|
+
const entries: DiagnosticDisplayEntry[] = [
|
|
108
|
+
{
|
|
109
|
+
diagnostic: makeDiag("PARSE_ERROR", "err1"),
|
|
110
|
+
location: "file1.sql:1",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
diagnostic: makeDiag("PARSE_ERROR", "err1"),
|
|
114
|
+
location: "file2.sql:5",
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
const items = buildDiagnosticDisplayItems(entries, false);
|
|
118
|
+
expect(items).toHaveLength(2);
|
|
119
|
+
expect(items[0].locations).toEqual(["file1.sql:1"]);
|
|
120
|
+
expect(items[1].locations).toEqual(["file2.sql:5"]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("ungrouped mode: entry without location gets empty locations array", () => {
|
|
124
|
+
const entries: DiagnosticDisplayEntry[] = [
|
|
125
|
+
{ diagnostic: makeDiag("PARSE_ERROR", "err1") },
|
|
126
|
+
];
|
|
127
|
+
const items = buildDiagnosticDisplayItems(entries, false);
|
|
128
|
+
expect(items[0].locations).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("grouped mode merges same code/message entries", () => {
|
|
132
|
+
const entries: DiagnosticDisplayEntry[] = [
|
|
133
|
+
{ diagnostic: makeDiag("PARSE_ERROR", "err1"), location: "a.sql:1" },
|
|
134
|
+
{ diagnostic: makeDiag("PARSE_ERROR", "err1"), location: "b.sql:2" },
|
|
135
|
+
{
|
|
136
|
+
diagnostic: makeDiag("UNRESOLVED_DEPENDENCY", "err2", "fix it"),
|
|
137
|
+
location: "c.sql:3",
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
const items = buildDiagnosticDisplayItems(entries, true);
|
|
141
|
+
expect(items).toHaveLength(2);
|
|
142
|
+
expect(items[0].locations).toEqual(["a.sql:1", "b.sql:2"]);
|
|
143
|
+
expect(items[1].code).toBe("UNRESOLVED_DEPENDENCY");
|
|
144
|
+
expect(items[1].suggestedFix).toBe("fix it");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("grouped mode: entry without location gets empty locations", () => {
|
|
148
|
+
const entries: DiagnosticDisplayEntry[] = [
|
|
149
|
+
{ diagnostic: makeDiag("PARSE_ERROR", "err1") },
|
|
150
|
+
];
|
|
151
|
+
const items = buildDiagnosticDisplayItems(entries, true);
|
|
152
|
+
expect(items[0].locations).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("grouped mode deduplicates identical locations", () => {
|
|
156
|
+
const entries: DiagnosticDisplayEntry[] = [
|
|
157
|
+
{ diagnostic: makeDiag("PARSE_ERROR", "err1"), location: "a.sql:1" },
|
|
158
|
+
{ diagnostic: makeDiag("PARSE_ERROR", "err1"), location: "a.sql:1" },
|
|
159
|
+
];
|
|
160
|
+
const items = buildDiagnosticDisplayItems(entries, true);
|
|
161
|
+
expect(items[0].locations).toEqual(["a.sql:1"]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("grouped mode preserves requiredObjectKey in group key", () => {
|
|
165
|
+
const entries: DiagnosticDisplayEntry[] = [
|
|
166
|
+
{
|
|
167
|
+
diagnostic: makeDiag("PARSE_ERROR", "err1"),
|
|
168
|
+
location: "a.sql:1",
|
|
169
|
+
requiredObjectKey: "key1",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
diagnostic: makeDiag("PARSE_ERROR", "err1"),
|
|
173
|
+
location: "b.sql:2",
|
|
174
|
+
requiredObjectKey: "key2",
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
const items = buildDiagnosticDisplayItems(entries, true);
|
|
178
|
+
expect(items).toHaveLength(2);
|
|
179
|
+
expect(items[0].requiredObjectKey).toBe("key1");
|
|
180
|
+
expect(items[1].requiredObjectKey).toBe("key2");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("resolveSqlFilePath", () => {
|
|
185
|
+
test("schemaPath is a directory", async () => {
|
|
186
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-apply-test-"));
|
|
187
|
+
try {
|
|
188
|
+
const result = await resolveSqlFilePath(dir, "schemas/table.sql");
|
|
189
|
+
expect(result).toBe(path.join(dir, "schemas/table.sql"));
|
|
190
|
+
} finally {
|
|
191
|
+
await rm(dir, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("schemaPath is a file uses dirname", async () => {
|
|
196
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-apply-test-"));
|
|
197
|
+
try {
|
|
198
|
+
const filePath = path.join(dir, "schema.sql");
|
|
199
|
+
await writeFile(filePath, "");
|
|
200
|
+
const result = await resolveSqlFilePath(filePath, "schemas/table.sql");
|
|
201
|
+
expect(result).toBe(path.join(dir, "schemas/table.sql"));
|
|
202
|
+
} finally {
|
|
203
|
+
await rm(dir, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("stat throws falls back to joining schemaPath as dir", async () => {
|
|
208
|
+
const result = await resolveSqlFilePath("/nonexistent/path", "table.sql");
|
|
209
|
+
expect(result).toBe(path.join("/nonexistent/path", "table.sql"));
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("formatStatementError", () => {
|
|
214
|
+
function makeErr(
|
|
215
|
+
overrides: Partial<StatementError> & {
|
|
216
|
+
id?: string;
|
|
217
|
+
sql?: string;
|
|
218
|
+
} = {},
|
|
219
|
+
): StatementError {
|
|
220
|
+
const { id, sql, ...rest } = overrides;
|
|
221
|
+
return {
|
|
222
|
+
message: "something failed",
|
|
223
|
+
code: "42601",
|
|
224
|
+
isDependencyError: false,
|
|
225
|
+
statement: { id: id ?? "raw-statement", sql: sql ?? "" },
|
|
226
|
+
...rest,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
test("minimal error with unparseable id", async () => {
|
|
231
|
+
const result = await formatStatementError(
|
|
232
|
+
makeErr({ id: "no-colon-here" }),
|
|
233
|
+
"/tmp",
|
|
234
|
+
);
|
|
235
|
+
expect(result).toContain("ERROR: something failed");
|
|
236
|
+
expect(result).toContain("SQL state: 42601");
|
|
237
|
+
expect(result).toContain("Location: no-colon-here");
|
|
238
|
+
expect(result).not.toContain("Detail:");
|
|
239
|
+
expect(result).not.toContain("Hint:");
|
|
240
|
+
expect(result).not.toContain("Character:");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("includes detail and hint when present", async () => {
|
|
244
|
+
const result = await formatStatementError(
|
|
245
|
+
makeErr({ detail: "some detail", hint: "try this" }),
|
|
246
|
+
"/tmp",
|
|
247
|
+
);
|
|
248
|
+
expect(result).toContain("Detail: some detail");
|
|
249
|
+
expect(result).toContain("Hint: try this");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("includes Character and Context when position is set", async () => {
|
|
253
|
+
const sql = "SELECT * FROM missing_table WHERE id = 1";
|
|
254
|
+
const result = await formatStatementError(
|
|
255
|
+
makeErr({ position: 15, sql, id: "raw-id" }),
|
|
256
|
+
"/tmp",
|
|
257
|
+
);
|
|
258
|
+
expect(result).toContain("Character: 15");
|
|
259
|
+
expect(result).toContain("Context:");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("parseable id with file containing the SQL resolves line:col", async () => {
|
|
263
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-apply-test-"));
|
|
264
|
+
try {
|
|
265
|
+
const sql = "CREATE TABLE foo (id int);";
|
|
266
|
+
const fileContent = `-- header\n\n${sql}\n`;
|
|
267
|
+
await writeFile(path.join(dir, "tables.sql"), fileContent);
|
|
268
|
+
|
|
269
|
+
const result = await formatStatementError(
|
|
270
|
+
makeErr({ id: "tables.sql:0", sql, position: 14 }),
|
|
271
|
+
dir,
|
|
272
|
+
);
|
|
273
|
+
expect(result).toMatch(/Location: tables\.sql:\d+:\d+/);
|
|
274
|
+
} finally {
|
|
275
|
+
await rm(dir, { recursive: true, force: true });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("parseable id with file containing SQL but no position resolves line only", async () => {
|
|
280
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-apply-test-"));
|
|
281
|
+
try {
|
|
282
|
+
const sql = "CREATE TABLE bar (id int);";
|
|
283
|
+
const fileContent = `-- header\n${sql}\n`;
|
|
284
|
+
await writeFile(path.join(dir, "tables.sql"), fileContent);
|
|
285
|
+
|
|
286
|
+
const result = await formatStatementError(
|
|
287
|
+
makeErr({ id: "tables.sql:0", sql }),
|
|
288
|
+
dir,
|
|
289
|
+
);
|
|
290
|
+
expect(result).toMatch(/Location: tables\.sql:\d+$/m);
|
|
291
|
+
expect(result).not.toMatch(/Location: tables\.sql:\d+:\d+/);
|
|
292
|
+
} finally {
|
|
293
|
+
await rm(dir, { recursive: true, force: true });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("parseable id with file but SQL not found falls back to statement index", async () => {
|
|
298
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-apply-test-"));
|
|
299
|
+
try {
|
|
300
|
+
await writeFile(
|
|
301
|
+
path.join(dir, "tables.sql"),
|
|
302
|
+
"-- completely different content",
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const result = await formatStatementError(
|
|
306
|
+
makeErr({
|
|
307
|
+
id: "tables.sql:2",
|
|
308
|
+
sql: "DROP TABLE nonexistent;",
|
|
309
|
+
}),
|
|
310
|
+
dir,
|
|
311
|
+
);
|
|
312
|
+
expect(result).toContain("Location: tables.sql (statement 2)");
|
|
313
|
+
} finally {
|
|
314
|
+
await rm(dir, { recursive: true, force: true });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("parseable id with position but file read fails uses SQL-based line/col", async () => {
|
|
319
|
+
const sql = "SELECT\n 1\n + bad_col";
|
|
320
|
+
const result = await formatStatementError(
|
|
321
|
+
makeErr({ id: "missing/file.sql:0", sql, position: 14 }),
|
|
322
|
+
"/nonexistent",
|
|
323
|
+
);
|
|
324
|
+
expect(result).toMatch(
|
|
325
|
+
/Location: missing\/file\.sql \(statement 0, line \d+, column \d+\)/,
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("parseable id without position and file read fails shows statement index only", async () => {
|
|
330
|
+
const result = await formatStatementError(
|
|
331
|
+
makeErr({ id: "missing/file.sql:1" }),
|
|
332
|
+
"/nonexistent",
|
|
333
|
+
);
|
|
334
|
+
expect(result).toContain("Location: missing/file.sql (statement 1)");
|
|
335
|
+
expect(result).not.toContain("line");
|
|
336
|
+
expect(result).not.toContain("column");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("all output lines are indented with two spaces", async () => {
|
|
340
|
+
const result = await formatStatementError(
|
|
341
|
+
makeErr({ detail: "d", hint: "h", position: 1, sql: "SELECT 1" }),
|
|
342
|
+
"/tmp",
|
|
343
|
+
);
|
|
344
|
+
for (const line of result.split("\n")) {
|
|
345
|
+
expect(line).toMatch(/^ {2}/);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
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
|
+
|
|
11
|
+
import { readFile, stat } from "node:fs/promises";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import type { Diagnostic } from "@supabase/pg-topo";
|
|
14
|
+
import type { StatementError } from "../../core/declarative-apply/round-apply.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert a 1-based character offset in a string to 1-based line and column.
|
|
18
|
+
* Used when mapping PostgreSQL error positions (in SQL) to file locations.
|
|
19
|
+
*/
|
|
20
|
+
export function positionToLineColumn(
|
|
21
|
+
sql: string,
|
|
22
|
+
position: number,
|
|
23
|
+
): { line: number; column: number } {
|
|
24
|
+
const lines = sql.split("\n");
|
|
25
|
+
let offset = 0;
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const lineLen = lines[i].length + (i < lines.length - 1 ? 1 : 0);
|
|
28
|
+
if (position <= offset + lineLen) {
|
|
29
|
+
return { line: i + 1, column: position - offset };
|
|
30
|
+
}
|
|
31
|
+
offset += lineLen;
|
|
32
|
+
}
|
|
33
|
+
const last = lines.length;
|
|
34
|
+
const lastLineLen = lines[last - 1]?.length ?? 0;
|
|
35
|
+
return { line: last, column: lastLineLen + 1 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse a statement id in the form "filePath:statementIndex" into components.
|
|
40
|
+
* The last colon separates path from index (paths may contain colons).
|
|
41
|
+
* Returns null if the format is invalid.
|
|
42
|
+
*/
|
|
43
|
+
function parseStatementId(
|
|
44
|
+
id: string,
|
|
45
|
+
): { filePath: string; statementIndex: number } | null {
|
|
46
|
+
const lastColon = id.lastIndexOf(":");
|
|
47
|
+
if (lastColon === -1) return null;
|
|
48
|
+
const filePath = id.slice(0, lastColon);
|
|
49
|
+
const n = Number.parseInt(id.slice(lastColon + 1), 10);
|
|
50
|
+
if (!Number.isInteger(n) || n < 0) return null;
|
|
51
|
+
return { filePath, statementIndex: n };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Input to buildDiagnosticDisplayItems: a pg-topo diagnostic plus optional location and object key. */
|
|
55
|
+
export type DiagnosticDisplayEntry = {
|
|
56
|
+
diagnostic: Diagnostic;
|
|
57
|
+
location?: string;
|
|
58
|
+
requiredObjectKey?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** One display row for a diagnostic (or a group of same-code diagnostics with multiple locations). */
|
|
62
|
+
type DiagnosticDisplayItem = {
|
|
63
|
+
code: string;
|
|
64
|
+
message: string;
|
|
65
|
+
suggestedFix?: string;
|
|
66
|
+
requiredObjectKey?: string;
|
|
67
|
+
locations: string[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Extract requiredObjectKey from a pg-topo diagnostic if present and non-empty. */
|
|
71
|
+
export const requiredObjectKeyFromDiagnostic = (
|
|
72
|
+
diagnostic: Diagnostic,
|
|
73
|
+
): string | undefined => {
|
|
74
|
+
const value = diagnostic.details?.requiredObjectKey;
|
|
75
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Build a stable key for grouping diagnostics with the same code, message, and suggested fix. */
|
|
79
|
+
const diagnosticDisplayGroupKey = (entry: DiagnosticDisplayEntry): string =>
|
|
80
|
+
[
|
|
81
|
+
entry.diagnostic.code,
|
|
82
|
+
entry.diagnostic.message,
|
|
83
|
+
entry.diagnostic.suggestedFix ?? "",
|
|
84
|
+
entry.requiredObjectKey ?? "",
|
|
85
|
+
].join("\u0000");
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Turn diagnostic entries into display items. If grouped is true, entries with
|
|
89
|
+
* the same code/message/suggestedFix are merged into one item with multiple locations.
|
|
90
|
+
*/
|
|
91
|
+
export const buildDiagnosticDisplayItems = (
|
|
92
|
+
entries: DiagnosticDisplayEntry[],
|
|
93
|
+
grouped: boolean,
|
|
94
|
+
): DiagnosticDisplayItem[] => {
|
|
95
|
+
if (!grouped) {
|
|
96
|
+
return entries.map((entry) => ({
|
|
97
|
+
code: entry.diagnostic.code,
|
|
98
|
+
message: entry.diagnostic.message,
|
|
99
|
+
suggestedFix: entry.diagnostic.suggestedFix,
|
|
100
|
+
requiredObjectKey: entry.requiredObjectKey,
|
|
101
|
+
locations: entry.location ? [entry.location] : [],
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const groupedItems = new Map<string, DiagnosticDisplayItem>();
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const key = diagnosticDisplayGroupKey(entry);
|
|
108
|
+
const existing = groupedItems.get(key);
|
|
109
|
+
if (!existing) {
|
|
110
|
+
groupedItems.set(key, {
|
|
111
|
+
code: entry.diagnostic.code,
|
|
112
|
+
message: entry.diagnostic.message,
|
|
113
|
+
suggestedFix: entry.diagnostic.suggestedFix,
|
|
114
|
+
requiredObjectKey: entry.requiredObjectKey,
|
|
115
|
+
locations: entry.location ? [entry.location] : [],
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (entry.location && !existing.locations.includes(entry.location)) {
|
|
120
|
+
existing.locations.push(entry.location);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [...groupedItems.values()];
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the full path to a .sql file from the schema path (directory or single file)
|
|
128
|
+
* and a relative file path (e.g. from a statement id). If schemaPath is a file, its
|
|
129
|
+
* directory is used as the base.
|
|
130
|
+
*/
|
|
131
|
+
export async function resolveSqlFilePath(
|
|
132
|
+
schemaPath: string,
|
|
133
|
+
relativeFilePath: string,
|
|
134
|
+
): Promise<string> {
|
|
135
|
+
try {
|
|
136
|
+
const statResult = await stat(schemaPath);
|
|
137
|
+
const baseDir = statResult.isFile() ? path.dirname(schemaPath) : schemaPath;
|
|
138
|
+
return path.join(baseDir, relativeFilePath);
|
|
139
|
+
} catch {
|
|
140
|
+
return path.join(schemaPath, relativeFilePath);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Find the 0-based start offset of statementSql in fileContent.
|
|
146
|
+
* Tries exact match first, then trimmed match. Returns -1 if not found.
|
|
147
|
+
*/
|
|
148
|
+
function findStatementStartInFile(
|
|
149
|
+
fileContent: string,
|
|
150
|
+
statementSql: string,
|
|
151
|
+
): number {
|
|
152
|
+
const exact = fileContent.indexOf(statementSql);
|
|
153
|
+
if (exact !== -1) return exact;
|
|
154
|
+
const trimmedStmt = statementSql.trim();
|
|
155
|
+
if (!trimmedStmt) return -1;
|
|
156
|
+
const trimmed = fileContent.indexOf(trimmedStmt);
|
|
157
|
+
if (trimmed !== -1) return trimmed;
|
|
158
|
+
return -1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Format a StatementError in pgAdmin-style: ERROR, Detail, SQL state, optional
|
|
163
|
+
* Context, Hint, and Location (resolving the .sql file and line/column when possible).
|
|
164
|
+
*/
|
|
165
|
+
export async function formatStatementError(
|
|
166
|
+
err: StatementError,
|
|
167
|
+
schemaPath: string,
|
|
168
|
+
): Promise<string> {
|
|
169
|
+
const lines: string[] = [];
|
|
170
|
+
lines.push(`ERROR: ${err.message}`);
|
|
171
|
+
if (err.detail) {
|
|
172
|
+
lines.push(`Detail: ${err.detail}`);
|
|
173
|
+
}
|
|
174
|
+
lines.push(`SQL state: ${err.code}`);
|
|
175
|
+
if (err.position !== undefined && err.statement.sql.length > 0) {
|
|
176
|
+
lines.push(`Character: ${err.position}`);
|
|
177
|
+
const pos = Math.max(
|
|
178
|
+
0,
|
|
179
|
+
Math.min(err.position - 1, err.statement.sql.length),
|
|
180
|
+
);
|
|
181
|
+
const contextStart = Math.max(0, pos - 40);
|
|
182
|
+
const contextEnd = Math.min(err.statement.sql.length, pos + 40);
|
|
183
|
+
const snippet = err.statement.sql.slice(contextStart, contextEnd);
|
|
184
|
+
const oneLine = snippet.replace(/\s+/g, " ").trim();
|
|
185
|
+
lines.push(`Context: ${oneLine || "(empty)"}`);
|
|
186
|
+
}
|
|
187
|
+
if (err.hint) {
|
|
188
|
+
lines.push(`Hint: ${err.hint}`);
|
|
189
|
+
}
|
|
190
|
+
const parsed = parseStatementId(err.statement.id);
|
|
191
|
+
if (parsed) {
|
|
192
|
+
let locationLine: string;
|
|
193
|
+
try {
|
|
194
|
+
const fullPath = await resolveSqlFilePath(schemaPath, parsed.filePath);
|
|
195
|
+
const fileContent = await readFile(fullPath, "utf-8");
|
|
196
|
+
const statementStart = findStatementStartInFile(
|
|
197
|
+
fileContent,
|
|
198
|
+
err.statement.sql,
|
|
199
|
+
);
|
|
200
|
+
if (statementStart !== -1) {
|
|
201
|
+
if (err.position !== undefined && err.statement.sql.length > 0) {
|
|
202
|
+
const fileErrorOffset = statementStart + (err.position - 1);
|
|
203
|
+
const fileErrorPosition = Math.min(
|
|
204
|
+
fileErrorOffset + 1,
|
|
205
|
+
fileContent.length,
|
|
206
|
+
);
|
|
207
|
+
const { line, column } = positionToLineColumn(
|
|
208
|
+
fileContent,
|
|
209
|
+
Math.max(1, fileErrorPosition),
|
|
210
|
+
);
|
|
211
|
+
locationLine = `Location: ${parsed.filePath}:${line}:${column}`;
|
|
212
|
+
} else {
|
|
213
|
+
const { line } = positionToLineColumn(
|
|
214
|
+
fileContent,
|
|
215
|
+
statementStart + 1,
|
|
216
|
+
);
|
|
217
|
+
locationLine = `Location: ${parsed.filePath}:${line}`;
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
locationLine = `Location: ${parsed.filePath} (statement ${parsed.statementIndex})`;
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
if (err.position !== undefined && err.statement.sql.length > 0) {
|
|
224
|
+
const { line, column } = positionToLineColumn(
|
|
225
|
+
err.statement.sql,
|
|
226
|
+
err.position,
|
|
227
|
+
);
|
|
228
|
+
locationLine = `Location: ${parsed.filePath} (statement ${parsed.statementIndex}, line ${line}, column ${column})`;
|
|
229
|
+
} else {
|
|
230
|
+
locationLine = `Location: ${parsed.filePath} (statement ${parsed.statementIndex})`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
lines.push(locationLine);
|
|
234
|
+
} else {
|
|
235
|
+
lines.push(`Location: ${err.statement.id}`);
|
|
236
|
+
}
|
|
237
|
+
return lines.map((l) => ` ${l}`).join("\n");
|
|
238
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { FileEntry } from "../../core/export/types.ts";
|
|
6
|
+
import { assertSafePath, computeFileDiff } from "./export-display.ts";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// assertSafePath
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
describe("assertSafePath", () => {
|
|
13
|
+
test("allows normal relative paths", () => {
|
|
14
|
+
expect(() =>
|
|
15
|
+
assertSafePath("schemas/public/tables/users.sql", "/tmp/out"),
|
|
16
|
+
).not.toThrow();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("allows nested paths", () => {
|
|
20
|
+
expect(() =>
|
|
21
|
+
assertSafePath("cluster/extensions/pgcrypto.sql", "/tmp/out"),
|
|
22
|
+
).not.toThrow();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("rejects path traversal with ..", () => {
|
|
26
|
+
expect(() => assertSafePath("../../etc/passwd", "/tmp/out")).toThrow(
|
|
27
|
+
"traversal",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("rejects path traversal embedded in path", () => {
|
|
32
|
+
expect(() =>
|
|
33
|
+
assertSafePath("schemas/../../../etc/passwd", "/tmp/out"),
|
|
34
|
+
).toThrow("traversal");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("rejects absolute paths", () => {
|
|
38
|
+
expect(() => assertSafePath("/etc/passwd", "/tmp/out")).toThrow(
|
|
39
|
+
"traversal",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// computeFileDiff – SQL-only filtering
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
describe("computeFileDiff", () => {
|
|
49
|
+
function makeFileEntry(relPath: string, sql = "-- content"): FileEntry {
|
|
50
|
+
return {
|
|
51
|
+
path: relPath,
|
|
52
|
+
order: 0,
|
|
53
|
+
statements: 1,
|
|
54
|
+
sql,
|
|
55
|
+
metadata: { objectType: "table" },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("non-SQL files in output dir are not marked as deleted", async () => {
|
|
60
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-export-test-"));
|
|
61
|
+
try {
|
|
62
|
+
await mkdir(path.join(dir, "schemas/public/tables"), { recursive: true });
|
|
63
|
+
await writeFile(
|
|
64
|
+
path.join(dir, "schemas/public/tables/users.sql"),
|
|
65
|
+
"-- users",
|
|
66
|
+
);
|
|
67
|
+
await writeFile(path.join(dir, "README.md"), "# readme");
|
|
68
|
+
await writeFile(path.join(dir, ".gitkeep"), "");
|
|
69
|
+
|
|
70
|
+
const newFiles = [makeFileEntry("schemas/public/tables/users.sql")];
|
|
71
|
+
const diff = await computeFileDiff(dir, newFiles);
|
|
72
|
+
|
|
73
|
+
expect(diff.deleted).not.toContain("README.md");
|
|
74
|
+
expect(diff.deleted).not.toContain(".gitkeep");
|
|
75
|
+
expect(diff.deleted).toHaveLength(0);
|
|
76
|
+
} finally {
|
|
77
|
+
await rm(dir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("stale SQL files are still marked as deleted", async () => {
|
|
82
|
+
const dir = await mkdtemp(path.join(tmpdir(), "pgd-export-test-"));
|
|
83
|
+
try {
|
|
84
|
+
await mkdir(path.join(dir, "schemas/public/tables"), { recursive: true });
|
|
85
|
+
await writeFile(
|
|
86
|
+
path.join(dir, "schemas/public/tables/users.sql"),
|
|
87
|
+
"-- users",
|
|
88
|
+
);
|
|
89
|
+
await writeFile(
|
|
90
|
+
path.join(dir, "schemas/public/tables/old_table.sql"),
|
|
91
|
+
"-- old",
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const newFiles = [makeFileEntry("schemas/public/tables/users.sql")];
|
|
95
|
+
const diff = await computeFileDiff(dir, newFiles);
|
|
96
|
+
|
|
97
|
+
expect(diff.deleted).toContain("schemas/public/tables/old_table.sql");
|
|
98
|
+
expect(diff.deleted).toHaveLength(1);
|
|
99
|
+
} finally {
|
|
100
|
+
await rm(dir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|