diff --git a/Makefile b/Makefile index 8a7f02b47e..ef86ec96da 100644 --- a/Makefile +++ b/Makefile @@ -88,6 +88,35 @@ migrate-check-validate: ## Validate migration checksums $(call print-target) atlas migrate --env local validate +NAMESPACE_FK_CHILD_TABLES := billing_profiles,billing_customer_overrides,billing_invoices +NAMESPACE_FK_SCHEMA := tools/migrate/schema/namespace_foreign_keys.sql +PG_SCHEMA_DIFF_DEV_DSN ?= postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable +PG_SCHEMA_DIFF_OUTPUT ?= - + +.PHONY: generate-namespace-fks +generate-namespace-fks: ## Generate additive namespace foreign keys from the Ent schema + $(call print-target) + go generate ./openmeter/ent/... + go run ./tools/migrate/cmd/namespacefks \ + -child-tables "$(NAMESPACE_FK_CHILD_TABLES)" \ + -output "$(NAMESPACE_FK_SCHEMA)" + +.PHONY: check-namespace-fks +check-namespace-fks: ## Ensure generated namespace foreign keys are current + $(call print-target) + go run ./tools/migrate/cmd/namespacefks \ + -child-tables "$(NAMESPACE_FK_CHILD_TABLES)" \ + -output "$(NAMESPACE_FK_SCHEMA)" \ + -check + +.PHONY: pgschema-diff-poc +pgschema-diff-poc: ## Generate a pg-schema-diff PoC plan from migrations to Ent, namespace FKs, and views + $(call print-target) + go run ./tools/migrate/cmd/pgschemadiff \ + -dev-dsn "$(PG_SCHEMA_DIFF_DEV_DSN)" \ + -namespace-fk-child-tables "$(NAMESPACE_FK_CHILD_TABLES)" \ + -output "$(PG_SCHEMA_DIFF_OUTPUT)" + .PHONY: generate-sqlc-testdata generate-sqlc-testdata: ## Generate SQLC testdata for a specific version (make generate-sqlc-testdata VERSION=20240826120919) $(call print-target) diff --git a/go.mod b/go.mod index 0efad9474b..667970bdd8 100644 --- a/go.mod +++ b/go.mod @@ -88,7 +88,10 @@ require ( google.golang.org/protobuf v1.36.11 ) +require github.com/stripe/pg-schema-diff v1.0.9 + require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/awalterschulze/goderive v0.5.1 // indirect github.com/bhmj/xpression v0.9.4 // indirect @@ -101,9 +104,18 @@ require ( github.com/go-test/deep v1.0.8 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/huandu/go-clone v1.7.3 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.14.3 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgtype v1.14.0 // indirect + github.com/jackc/pgx/v4 v4.18.2 // indirect github.com/jmattheis/goverter v1.9.3 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/kisielk/gotool v1.0.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 // indirect github.com/oasdiff/yaml v0.1.1 // indirect github.com/oasdiff/yaml3 v0.0.14 // indirect @@ -112,6 +124,7 @@ require ( github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/rickb777/plural/v2 v2.1.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/samber/slog-common v0.21.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/speakeasy-api/jsonpath v0.6.3 // indirect diff --git a/go.sum b/go.sum index ff5a2d1cea..2652a4c87d 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,7 @@ github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl github.com/AppsFlyer/go-sundheit v0.6.0/go.mod h1:LDdBHD6tQBtmHsdW+i1GwdTt6Wqc0qazf5ZEJVTbTME= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ClickHouse/ch-go v0.74.0 h1:uYs2m4wIt0ZHSM1E72rg0maCfzhR2V3xWb/vZEgpeWE= github.com/ClickHouse/ch-go v0.74.0/go.mod h1:sZ/r+8ttZMjyrP9PuFbgoVbth1ywIu2LIQNA2vgko6M= github.com/ClickHouse/clickhouse-go/v2 v2.47.0 h1:ZDAzrnKSOPTIsm4tdUNfrii2yc8dk4SVRLC77BR7Z5Q= @@ -18,6 +19,7 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/IBM/sarama v1.60.1 h1:2IjpLPCL16CvaJcpxUT5+zE6tpeY5HdhREZOES80kGE= github.com/IBM/sarama v1.60.1/go.mod h1:ugg061kdM8zE4mgCeCUwDMd9NRd7QIRMoiA4a/Z8VH8= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -79,13 +81,19 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/confluentinc/confluent-kafka-go/v2 v2.15.0 h1:Nfz04XU4qtT4/OU3zibwJVjUYs5SG35d2SwBIQ+L2FY= github.com/confluentinc/confluent-kafka-go/v2 v2.15.0/go.mod h1:uvixf1aKCnE5NHlELzZpO4k6TQc1DJalz67dVGaYxIs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -145,6 +153,8 @@ github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AY github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -158,11 +168,14 @@ github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAg github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-slog/otelslog v0.3.0 h1:O/ettamJL9sJKCmtEa/xHF5dqHvWF6xION/NIiZJ6gk= github.com/go-slog/otelslog v0.3.0/go.mod h1:TxQTymq11rhMaNLE4yMe3kXuUf9ksoGyN9ivieHFWu8= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-cz/devslog v0.0.17 h1:FAAqtWwomNWqWaoiitKfKWwSU8la0SQ9XhwXF7x2QF4= @@ -190,6 +203,7 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= @@ -240,12 +254,55 @@ github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8i github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= @@ -276,15 +333,22 @@ github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJn github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= @@ -293,14 +357,21 @@ github.com/lmittmann/tint v1.2.0 h1:AogHRHy8HUJUnNJBHJlYa+fR4YY8mko2cnCp67xn9JY= github.com/lmittmann/tint v1.2.0/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/magefile/mage v1.17.2 h1:fyXVu1eadI8Ap1HCCNgEhJ5McIWiYhLR8uol64ZZc40= github.com/magefile/mage v1.17.2/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -373,6 +444,7 @@ github.com/peterldowns/testy v0.0.1 h1:9a6LzvnKcL52Crzud1z7jbsAojTntCh89ho6mgsr4 github.com/peterldowns/testy v0.0.1/go.mod h1:J4sm75UEzbfBIcq0zbrshWWjsJQiJ5RrhTPYKVY2Ww8= github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -407,8 +479,13 @@ github.com/rickb777/plural/v2 v2.1.0 h1:2TsGzyoTSRnXPsyEV3j2fET9s8nz6ThEXlBXxg6q github.com/rickb777/plural/v2 v2.1.0/go.mod h1:VQ7T01f3Xi9EqnIE0vMBw3351Bj2xWVle4TOccdlB7I= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= @@ -424,13 +501,18 @@ github.com/samber/slog-multi v1.8.0 h1:E05c1wnQ+8M58oQDBABlJ4TEIJWssNgtckso3zlaL github.com/samber/slog-multi v1.8.0/go.mod h1:6+3j/ILxDvAcLD75YdQAm6iKWu6AmwlohLgQxL/2aiI= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= @@ -452,11 +534,13 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -467,6 +551,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stripe/pg-schema-diff v1.0.9 h1:oI1MPWAEuvuc/TCeGoJJ6sc+70Rs+epFM3LXvTFZxYE= +github.com/stripe/pg-schema-diff v1.0.9/go.mod h1:3IctPaAqm+0LtWw/GiwyRoRlU1/N/+00+eXVk0KZIHs= github.com/stripe/stripe-go/v80 v80.2.1 h1:1FQP5a/gpC4i0ezS8EPqdme3K/H9UlNWswqNHFekieY= github.com/stripe/stripe-go/v80 v80.2.1/go.mod h1:n7tsDvdltYlzOLGXlseMSJM6ik5uv3guptqtae/VSak= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -508,6 +594,7 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/otelslog v0.19.0 h1:5RgvxieNq9tS3ewrV1vnODvbHPfKUIJcYtF9Cvz+6aQ= @@ -548,10 +635,21 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/ go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -559,22 +657,33 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -595,13 +704,21 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -615,13 +732,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= @@ -629,13 +749,22 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -659,10 +788,13 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -678,3 +810,4 @@ gotest.tools/gotestsum v1.13.0 h1:+Lh454O9mu9AMG1APV4o0y7oDYKyik/3kBOiCqiEpRo= gotest.tools/gotestsum v1.13.0/go.mod h1:7f0NS5hFb0dWr4NtcsAsF0y1kzjEFfAil0HiBQJE03Q= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/tools/migrate/README.md b/tools/migrate/README.md index 5ddf0b1e4f..0217cffa23 100644 --- a/tools/migrate/README.md +++ b/tools/migrate/README.md @@ -7,3 +7,27 @@ make generate-view-sql ``` This writes `tools/migrate/views.sql` by loading `openmeter/ent/schema` via Ent's schema loader and emitting Postgres `CREATE VIEW` statements from `EntSQL` view annotations. + +## pg-schema-diff proof of concept + +Generate a review-only SQL plan from the current migration history to the +desired Ent schema, generated namespace foreign keys, and Ent-managed views: + +```bash +make pgschema-diff-poc +``` + +The command creates and drops disposable databases on `PG_SCHEMA_DIFF_DEV_DSN`. +It does not mutate an existing application database, but the configured role +must be allowed to create databases. Set `PG_SCHEMA_DIFF_OUTPUT` to write the +plan to a file instead of stdout. The migration state table (`schema_om`) is +removed from both disposable databases before comparison, so it cannot become +part of the generated application migration. The rendered SQL also removes the +`public` schema qualification to match the existing migration style. + +This is intentionally a proof of concept. The output is not added to the +migration directory, no down migration is generated, and `atlas.sum` is not +updated. The generated plan is replay-validated against another disposable +database by default; pass `-skip-plan-validation` directly to the command only +when investigating pg-schema-diff behavior. Review the emitted statements and +hazard comments before using them in a migration. diff --git a/tools/migrate/cmd/namespacefks/main.go b/tools/migrate/cmd/namespacefks/main.go new file mode 100644 index 0000000000..9a6114f81f --- /dev/null +++ b/tools/migrate/cmd/namespacefks/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "os" + "path/filepath" + "strings" + + entmigrate "github.com/openmeterio/openmeter/openmeter/ent/db/migrate" + "github.com/openmeterio/openmeter/tools/migrate/namespacefks" +) + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run() error { + var ( + childTables string + check bool + output string + ) + + flag.StringVar(&childTables, "child-tables", "", "comma-separated child tables to include; empty includes all eligible tables") + flag.BoolVar(&check, "check", false, "fail when the generated SQL differs from the output file") + flag.StringVar(&output, "output", "-", "output file, or - for stdout") + flag.Parse() + + generated, err := namespacefks.Generate(namespacefks.GenerateInput{ + Tables: entmigrate.Tables, + ChildTables: splitCommaSeparated(childTables), + }) + if err != nil { + return err + } + + if output == "-" { + if check { + return errors.New("check requires an output file") + } + + _, err := os.Stdout.Write(generated) + return err + } + + if check { + existing, err := os.ReadFile(output) + if err != nil { + return fmt.Errorf("read generated namespace foreign keys: %w", err) + } + + if !bytes.Equal(existing, generated) { + return fmt.Errorf("%s is stale; run make generate-namespace-fks", output) + } + + return nil + } + + if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil { + return fmt.Errorf("create output directory: %w", err) + } + + temporary, err := os.CreateTemp(filepath.Dir(output), ".namespace-fks-*.sql") + if err != nil { + return fmt.Errorf("create temporary output: %w", err) + } + temporaryName := temporary.Name() + defer os.Remove(temporaryName) + + if err := temporary.Chmod(0o644); err != nil { + _ = temporary.Close() + return fmt.Errorf("set temporary output permissions: %w", err) + } + + if _, err := temporary.Write(generated); err != nil { + _ = temporary.Close() + return fmt.Errorf("write temporary output: %w", err) + } + + if err := temporary.Close(); err != nil { + return fmt.Errorf("close temporary output: %w", err) + } + + if err := os.Rename(temporaryName, output); err != nil { + return fmt.Errorf("replace generated namespace foreign keys: %w", err) + } + + return nil +} + +func splitCommaSeparated(value string) []string { + if strings.TrimSpace(value) == "" { + return nil + } + + parts := strings.Split(value, ",") + tables := make([]string, 0, len(parts)) + seen := make(map[string]struct{}, len(parts)) + for _, part := range parts { + table := strings.TrimSpace(part) + if table == "" { + continue + } + if _, ok := seen[table]; ok { + continue + } + + seen[table] = struct{}{} + tables = append(tables, table) + } + + return tables +} diff --git a/tools/migrate/cmd/pgschemadiff/main.go b/tools/migrate/cmd/pgschemadiff/main.go new file mode 100644 index 0000000000..af1358b5dc --- /dev/null +++ b/tools/migrate/cmd/pgschemadiff/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "log/slog" + "os" + "os/signal" + "path/filepath" + "strings" + + "github.com/openmeterio/openmeter/tools/migrate/pgschemadiff" +) + +const defaultNamespaceChildTables = "billing_profiles,billing_customer_overrides,billing_invoices" + +func main() { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + if err := run(ctx, os.Args[1:], os.Stdout, os.Stderr); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(ctx context.Context, args []string, stdout, stderr io.Writer) error { + flags := flag.NewFlagSet("pgschemadiff", flag.ContinueOnError) + flags.SetOutput(stderr) + + devDatabaseURL := flags.String("dev-dsn", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable", "PostgreSQL instance on which disposable databases can be created") + entSchemaPath := flags.String("ent-schema", "./openmeter/ent/schema", "path to the Ent schema package") + namespaceChildTables := flags.String("namespace-fk-child-tables", defaultNamespaceChildTables, "comma-separated child tables for generated namespace foreign keys") + skipPlanValidation := flags.Bool("skip-plan-validation", false, "skip replaying the generated plan against a disposable database") + outputPath := flags.String("output", "-", "output SQL file, or - for stdout") + if err := flags.Parse(args); err != nil { + return err + } + + logger := slog.New(slog.NewTextHandler(stderr, &slog.HandlerOptions{Level: slog.LevelWarn})) + plan, err := pgschemadiff.GeneratePlan(ctx, pgschemadiff.GeneratePlanInput{ + DevDatabaseURL: *devDatabaseURL, + EntSchemaPath: *entSchemaPath, + NamespaceChildTables: splitCommaSeparated(*namespaceChildTables), + SkipPlanValidation: *skipPlanValidation, + Logger: logger, + }) + if err != nil { + return err + } + + output := pgschemadiff.RenderSQL(plan, !*skipPlanValidation) + if *outputPath == "-" { + _, err := stdout.Write(output) + return err + } + + if err := os.MkdirAll(filepath.Dir(*outputPath), 0o755); err != nil { + return fmt.Errorf("create output directory: %w", err) + } + if err := os.WriteFile(*outputPath, output, 0o644); err != nil { + return fmt.Errorf("write schema diff: %w", err) + } + + return nil +} + +func splitCommaSeparated(value string) []string { + parts := strings.Split(value, ",") + values := make([]string, 0, len(parts)) + seen := make(map[string]struct{}, len(parts)) + for _, part := range parts { + value := strings.TrimSpace(part) + if value == "" { + continue + } + if _, ok := seen[value]; ok { + continue + } + + seen[value] = struct{}{} + values = append(values, value) + } + + return values +} diff --git a/tools/migrate/namespacefks/generator.go b/tools/migrate/namespacefks/generator.go new file mode 100644 index 0000000000..aab7dec485 --- /dev/null +++ b/tools/migrate/namespacefks/generator.go @@ -0,0 +1,247 @@ +package namespacefks + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "slices" + "strings" + + "entgo.io/ent/dialect/sql/schema" +) + +const ( + namespaceColumn = "namespace" + idColumn = "id" + + postgresIdentifierMaxLength = 63 +) + +type GenerateInput struct { + Tables []*schema.Table + ChildTables []string +} + +func (i GenerateInput) Validate() error { + var errs []error + + if len(i.Tables) == 0 { + errs = append(errs, errors.New("tables are required")) + } + + for _, table := range i.Tables { + if table == nil { + errs = append(errs, errors.New("tables cannot contain nil")) + } + } + + for _, table := range i.ChildTables { + if strings.TrimSpace(table) == "" { + errs = append(errs, errors.New("child table names cannot be empty")) + } + } + + return errors.Join(errs...) +} + +type namespaceForeignKey struct { + ChildTable string + ForeignKeyColumn string + OriginalSymbol string + ParentTable string +} + +func Generate(input GenerateInput) ([]byte, error) { + if err := input.Validate(); err != nil { + return nil, fmt.Errorf("invalid namespace foreign key generation input: %w", err) + } + + tablesByName := make(map[string]*schema.Table, len(input.Tables)) + for _, table := range input.Tables { + tablesByName[table.Name] = table + } + + includedChildTables := make(map[string]struct{}, len(input.ChildTables)) + for _, table := range input.ChildTables { + includedChildTables[table] = struct{}{} + if _, ok := tablesByName[table]; !ok { + return nil, fmt.Errorf("child table %q not found", table) + } + } + + var foreignKeys []namespaceForeignKey + parents := make(map[string]*schema.Table) + + for _, child := range input.Tables { + if len(includedChildTables) > 0 { + if _, ok := includedChildTables[child.Name]; !ok { + continue + } + } + + if !child.HasColumn(namespaceColumn) { + continue + } + + for _, foreignKey := range child.ForeignKeys { + if !isNamespaceForeignKeyCandidate(child, foreignKey) { + continue + } + + foreignKeys = append(foreignKeys, namespaceForeignKey{ + ChildTable: child.Name, + ForeignKeyColumn: foreignKey.Columns[0].Name, + OriginalSymbol: foreignKey.Symbol, + ParentTable: foreignKey.RefTable.Name, + }) + parents[foreignKey.RefTable.Name] = foreignKey.RefTable + } + } + + slices.SortFunc(foreignKeys, func(left, right namespaceForeignKey) int { + if byChild := strings.Compare(left.ChildTable, right.ChildTable); byChild != 0 { + return byChild + } + + return strings.Compare(left.ForeignKeyColumn, right.ForeignKeyColumn) + }) + + parentNames := make([]string, 0, len(parents)) + for parentName := range parents { + parentNames = append(parentNames, parentName) + } + slices.Sort(parentNames) + + var output bytes.Buffer + output.WriteString("-- Code generated by make generate-namespace-fks; DO NOT EDIT.\n") + output.WriteString("-- The original Ent foreign keys remain responsible for relationship lifecycle actions.\n") + output.WriteString("-- These additive constraints only enforce namespace equality.\n") + if len(input.ChildTables) > 0 { + selectedTables := slices.Clone(input.ChildTables) + slices.Sort(selectedTables) + fmt.Fprintf(&output, "-- Child tables: %s.\n", strings.Join(selectedTables, ", ")) + } + + for _, parentName := range parentNames { + parent := parents[parentName] + if hasUniqueNamespaceID(parent) { + continue + } + + fmt.Fprintf( + &output, + "\n-- Required by namespace guard foreign keys referencing %s.\nCREATE UNIQUE INDEX %s ON %s (%s, %s);\n", + quoteIdentifier(parent.Name), + quoteIdentifier(postgresIdentifier(parent.Name+"_namespace_id_key")), + quoteIdentifier(parent.Name), + quoteIdentifier(namespaceColumn), + quoteIdentifier(idColumn), + ) + } + + for _, foreignKey := range foreignKeys { + fmt.Fprintf( + &output, + "\n-- Extends Ent foreign key %s with namespace equality.\nALTER TABLE %s\n ADD CONSTRAINT %s\n FOREIGN KEY (%s, %s)\n REFERENCES %s (%s, %s)\n ON UPDATE NO ACTION\n ON DELETE NO ACTION;\n", + quoteIdentifier(foreignKey.OriginalSymbol), + quoteIdentifier(foreignKey.ChildTable), + quoteIdentifier(postgresIdentifier(strings.Join([]string{ + foreignKey.ChildTable, + strings.TrimSuffix(foreignKey.ForeignKeyColumn, "_id"), + "namespace_guard_fk", + }, "_"))), + quoteIdentifier(namespaceColumn), + quoteIdentifier(foreignKey.ForeignKeyColumn), + quoteIdentifier(foreignKey.ParentTable), + quoteIdentifier(namespaceColumn), + quoteIdentifier(idColumn), + ) + } + + return output.Bytes(), nil +} + +func isNamespaceForeignKeyCandidate(child *schema.Table, foreignKey *schema.ForeignKey) bool { + if foreignKey == nil || foreignKey.RefTable == nil { + return false + } + + if len(foreignKey.Columns) != 1 || len(foreignKey.RefColumns) != 1 { + return false + } + + if foreignKey.Columns[0].Name == namespaceColumn || foreignKey.RefColumns[0].Name != idColumn { + return false + } + + if !foreignKey.RefTable.HasColumn(namespaceColumn) { + return false + } + + return !hasNamespaceForeignKey(child, foreignKey) +} + +func hasNamespaceForeignKey(child *schema.Table, candidate *schema.ForeignKey) bool { + for _, foreignKey := range child.ForeignKeys { + if foreignKey == nil || foreignKey.RefTable == nil || foreignKey.RefTable.Name != candidate.RefTable.Name { + continue + } + + if columnNamesEqual(foreignKey.Columns, namespaceColumn, candidate.Columns[0].Name) && + columnNamesEqual(foreignKey.RefColumns, namespaceColumn, idColumn) { + return true + } + } + + return false +} + +func hasUniqueNamespaceID(table *schema.Table) bool { + if columnNamesEqual(table.PrimaryKey, namespaceColumn, idColumn) { + return true + } + + for _, index := range table.Indexes { + if !index.Unique || !columnNamesEqual(index.Columns, namespaceColumn, idColumn) { + continue + } + + if index.Annotation == nil || index.Annotation.Where == "" { + return true + } + } + + return false +} + +func columnNamesEqual(columns []*schema.Column, expected ...string) bool { + if len(columns) != len(expected) { + return false + } + + for index, column := range columns { + if column == nil || column.Name != expected[index] { + return false + } + } + + return true +} + +func quoteIdentifier(identifier string) string { + return `"` + strings.ReplaceAll(identifier, `"`, `""`) + `"` +} + +func postgresIdentifier(identifier string) string { + if len(identifier) <= postgresIdentifierMaxLength { + return identifier + } + + hash := sha256.Sum256([]byte(identifier)) + suffix := hex.EncodeToString(hash[:4]) + prefixLength := postgresIdentifierMaxLength - len(suffix) - 1 + + return strings.TrimRight(identifier[:prefixLength], "_") + "_" + suffix +} diff --git a/tools/migrate/namespacefks/generator_test.go b/tools/migrate/namespacefks/generator_test.go new file mode 100644 index 0000000000..6b7ca0e31a --- /dev/null +++ b/tools/migrate/namespacefks/generator_test.go @@ -0,0 +1,156 @@ +package namespacefks_test + +import ( + "database/sql" + "strings" + "testing" + + "entgo.io/ent/dialect/sql/schema" + "github.com/stretchr/testify/require" + + entmigrate "github.com/openmeterio/openmeter/openmeter/ent/db/migrate" + "github.com/openmeterio/openmeter/openmeter/testutils" + "github.com/openmeterio/openmeter/tools/migrate/namespacefks" +) + +func TestGenerateBillingNamespaceForeignKeys(t *testing.T) { + // given: + // - the generated Ent schema and the billing tables that persist configuration and invoice snapshots + // when: + // - namespace guard SQL is generated from their existing ID-only foreign keys + // then: + // - all 13 eligible billing relationships and the missing parent index are represented + output, err := namespacefks.Generate(namespacefks.GenerateInput{ + Tables: entmigrate.Tables, + ChildTables: []string{ + "billing_profiles", + "billing_customer_overrides", + "billing_invoices", + }, + }) + require.NoError(t, err) + + sql := string(output) + require.Equal(t, 13, strings.Count(sql, "ADD CONSTRAINT")) + require.Equal(t, 1, strings.Count(sql, "CREATE UNIQUE INDEX")) + + for _, relationship := range []string{ + `FOREIGN KEY ("namespace", "billing_profile_id")`, + `FOREIGN KEY ("namespace", "customer_id")`, + `FOREIGN KEY ("namespace", "invoicing_app_id")`, + `FOREIGN KEY ("namespace", "payment_app_id")`, + `FOREIGN KEY ("namespace", "source_billing_profile_id")`, + `FOREIGN KEY ("namespace", "tax_app_id")`, + `FOREIGN KEY ("namespace", "tax_code_id")`, + `FOREIGN KEY ("namespace", "workflow_config_id")`, + } { + require.Contains(t, sql, relationship) + } + + require.Contains(t, sql, `CREATE UNIQUE INDEX "billing_workflow_configs_namespace_id_key"`) +} + +func TestGenerateRejectsUnknownChildTable(t *testing.T) { + // given: + // - an unknown child table selection + // when: + // - namespace guard SQL generation starts + // then: + // - generation fails instead of silently producing an incomplete overlay + _, err := namespacefks.Generate(namespacefks.GenerateInput{ + Tables: entmigrate.Tables, + ChildTables: []string{"missing_table"}, + }) + require.EqualError(t, err, `child table "missing_table" not found`) +} + +func TestGeneratedNamespaceForeignKeyGuardsCrossNamespaceReferences(t *testing.T) { + // given: + // - two namespace-owned tables connected by an ID-only foreign key with ON DELETE SET NULL + // when: + // - the generated namespace guard is applied + // then: + // - cross-namespace writes fail and the original delete behavior remains intact + testDB := testutils.InitPostgresDB(t, testutils.PostgresDBStateEmpty) + defer testDB.Close(t) + + _, err := testDB.PGDriver.DB().Exec(` + CREATE TABLE parents ( + id text PRIMARY KEY, + namespace text NOT NULL + ); + CREATE TABLE children ( + id text PRIMARY KEY, + namespace text NOT NULL, + parent_id text, + CONSTRAINT children_parent_fk + FOREIGN KEY (parent_id) REFERENCES parents (id) ON DELETE SET NULL + ); + `) + require.NoError(t, err) + + parentID := &schema.Column{Name: "id"} + parentNamespace := &schema.Column{Name: "namespace"} + parent := &schema.Table{ + Name: "parents", + Columns: []*schema.Column{parentID, parentNamespace}, + PrimaryKey: []*schema.Column{parentID}, + } + childID := &schema.Column{Name: "id"} + childNamespace := &schema.Column{Name: "namespace"} + childParentID := &schema.Column{Name: "parent_id", Nullable: true} + child := &schema.Table{ + Name: "children", + Columns: []*schema.Column{childID, childNamespace, childParentID}, + PrimaryKey: []*schema.Column{childID}, + ForeignKeys: []*schema.ForeignKey{{ + Symbol: "children_parent_fk", + Columns: []*schema.Column{childParentID}, + RefTable: parent, + RefColumns: []*schema.Column{parentID}, + }}, + } + + generated, err := namespacefks.Generate(namespacefks.GenerateInput{ + Tables: []*schema.Table{parent, child}, + ChildTables: []string{"children"}, + }) + require.NoError(t, err) + + _, err = testDB.PGDriver.DB().Exec(string(generated)) + require.NoError(t, err) + + _, err = testDB.PGDriver.DB().Exec(` + INSERT INTO parents (id, namespace) + VALUES ('parent-a', 'namespace-a'), ('parent-b', 'namespace-b') + `) + require.NoError(t, err) + + _, err = testDB.PGDriver.DB().Exec(` + INSERT INTO children (id, namespace, parent_id) + VALUES ('invalid-child', 'namespace-a', 'parent-b') + `) + require.Error(t, err) + + _, err = testDB.PGDriver.DB().Exec(` + INSERT INTO children (id, namespace, parent_id) + VALUES ('valid-child', 'namespace-a', 'parent-a') + `) + require.NoError(t, err) + + _, err = testDB.PGDriver.DB().Exec(`DELETE FROM parents WHERE id = 'parent-a'`) + require.NoError(t, err) + + var ( + namespace string + parentRef sql.NullString + ) + err = testDB.PGDriver.DB().QueryRow(` + SELECT namespace, parent_id + FROM children + WHERE id = 'valid-child' + `).Scan(&namespace, &parentRef) + require.NoError(t, err) + require.Equal(t, "namespace-a", namespace) + require.False(t, parentRef.Valid) +} diff --git a/tools/migrate/pgschemadiff/pgschemadiff.go b/tools/migrate/pgschemadiff/pgschemadiff.go new file mode 100644 index 0000000000..d5a1ecf08f --- /dev/null +++ b/tools/migrate/pgschemadiff/pgschemadiff.go @@ -0,0 +1,255 @@ +package pgschemadiff + +import ( + "bytes" + "context" + "database/sql" + "errors" + "fmt" + "log/slog" + "net/url" + "strings" + + "entgo.io/ent/dialect" + entsql "entgo.io/ent/dialect/sql" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/stdlib" + pgdiff "github.com/stripe/pg-schema-diff/pkg/diff" + "github.com/stripe/pg-schema-diff/pkg/tempdb" + + entmigrate "github.com/openmeterio/openmeter/openmeter/ent/db/migrate" + "github.com/openmeterio/openmeter/pkg/models" + ommigrate "github.com/openmeterio/openmeter/tools/migrate" + "github.com/openmeterio/openmeter/tools/migrate/namespacefks" + "github.com/openmeterio/openmeter/tools/migrate/viewgen" +) + +const temporaryDatabaseMaxConnections = 5 + +var publicSchemaQualifier = strings.NewReplacer( + `"public".`, "", + "public.", "", +) + +type GeneratePlanInput struct { + DevDatabaseURL string + EntSchemaPath string + NamespaceChildTables []string + SkipPlanValidation bool + Logger *slog.Logger +} + +func (i GeneratePlanInput) Validate() error { + var errs []error + + if strings.TrimSpace(i.DevDatabaseURL) == "" { + errs = append(errs, errors.New("dev database URL is required")) + } + if strings.TrimSpace(i.EntSchemaPath) == "" { + errs = append(errs, errors.New("ent schema path is required")) + } + if i.Logger == nil { + errs = append(errs, errors.New("logger is required")) + } + + return models.NewNillableGenericValidationError(errors.Join(errs...)) +} + +// GeneratePlan compares the existing migration history with a disposable +// database reconciled to the Ent schema, namespace foreign keys, and Ent views. +// The configured PostgreSQL instance is used only for disposable databases. +func GeneratePlan(ctx context.Context, input GeneratePlanInput) (_ pgdiff.Plan, retErr error) { + if err := input.Validate(); err != nil { + return pgdiff.Plan{}, fmt.Errorf("invalid pg-schema-diff input: %w", err) + } + + baseConfig, err := pgx.ParseConfig(input.DevDatabaseURL) + if err != nil { + return pgdiff.Plan{}, fmt.Errorf("parse dev database URL: %w", err) + } + + factory, err := tempdb.NewOnInstanceFactory( + ctx, + newConnectionPoolFactory(baseConfig), + tempdb.WithRootDatabase(baseConfig.Database), + ) + if err != nil { + return pgdiff.Plan{}, fmt.Errorf("create temporary database factory: %w", err) + } + defer func() { + retErr = errors.Join(retErr, factory.Close()) + }() + + currentDatabase, err := createMigratedDatabase(ctx, factory, input.DevDatabaseURL, input.Logger) + if err != nil { + return pgdiff.Plan{}, fmt.Errorf("create current migration database: %w", err) + } + defer func() { + retErr = errors.Join(retErr, currentDatabase.Close(ctx)) + }() + + desiredDatabase, err := createMigratedDatabase(ctx, factory, input.DevDatabaseURL, input.Logger) + if err != nil { + return pgdiff.Plan{}, fmt.Errorf("create desired migration database: %w", err) + } + defer func() { + retErr = errors.Join(retErr, desiredDatabase.Close(ctx)) + }() + + if err := reconcileDesiredDatabase(ctx, desiredDatabase.ConnPool, input); err != nil { + return pgdiff.Plan{}, fmt.Errorf("reconcile desired database: %w", err) + } + + options := []pgdiff.PlanOpt{ + pgdiff.WithTempDbFactory(factory), + pgdiff.WithIncludeSchemas("public"), + } + if input.SkipPlanValidation { + options = append(options, pgdiff.WithDoNotValidatePlan()) + } + + plan, err := pgdiff.Generate( + ctx, + pgdiff.DBSchemaSource(currentDatabase.ConnPool), + pgdiff.DBSchemaSource(desiredDatabase.ConnPool), + options..., + ) + if err != nil { + return pgdiff.Plan{}, fmt.Errorf("generate schema diff: %w", err) + } + + return plan, nil +} + +func newConnectionPoolFactory(baseConfig *pgx.ConnConfig) tempdb.CreateConnPoolForDbFn { + return func(ctx context.Context, databaseName string) (*sql.DB, error) { + config := baseConfig.Copy() + config.Database = databaseName + + database := stdlib.OpenDB(*config) + database.SetMaxOpenConns(temporaryDatabaseMaxConnections) + if err := database.PingContext(ctx); err != nil { + _ = database.Close() + return nil, fmt.Errorf("ping database %q: %w", databaseName, err) + } + + return database, nil + } +} + +func createMigratedDatabase(ctx context.Context, factory tempdb.Factory, devDatabaseURL string, logger *slog.Logger) (_ *tempdb.Database, retErr error) { + database, err := factory.Create(ctx) + if err != nil { + return nil, fmt.Errorf("create temporary database: %w", err) + } + defer func() { + if retErr != nil { + retErr = errors.Join(retErr, database.Close(ctx)) + } + }() + + var databaseName string + if err := database.ConnPool.QueryRowContext(ctx, "SELECT current_database()").Scan(&databaseName); err != nil { + return nil, fmt.Errorf("query temporary database name: %w", err) + } + + temporaryDatabaseURL, err := databaseURL(devDatabaseURL, databaseName) + if err != nil { + return nil, fmt.Errorf("build temporary database URL: %w", err) + } + migrator, err := ommigrate.New(ommigrate.MigrateOptions{ + ConnectionString: temporaryDatabaseURL, + Migrations: ommigrate.OMMigrationsConfig, + Logger: logger, + }) + if err != nil { + return nil, fmt.Errorf("create migrator: %w", err) + } + defer migrator.CloseOrLogError() + + if err := migrator.Up(); err != nil { + return nil, fmt.Errorf("apply migration history: %w", err) + } + if _, err := database.ConnPool.ExecContext(ctx, `DROP TABLE IF EXISTS "schema_om"`); err != nil { + return nil, fmt.Errorf("exclude migration state table: %w", err) + } + + return database, nil +} + +func databaseURL(baseURL, databaseName string) (string, error) { + parsedURL, err := url.Parse(baseURL) + if err != nil { + return "", fmt.Errorf("parse database URL: %w", err) + } + if parsedURL.Scheme != "postgres" && parsedURL.Scheme != "postgresql" { + return "", fmt.Errorf("unsupported database URL scheme %q", parsedURL.Scheme) + } + + parsedURL.Path = "/" + databaseName + return parsedURL.String(), nil +} + +func reconcileDesiredDatabase(ctx context.Context, database *sql.DB, input GeneratePlanInput) error { + driver := entsql.OpenDB(dialect.Postgres, database) + if err := entmigrate.NewSchema(driver).Create( + ctx, + entmigrate.WithDropColumn(true), + entmigrate.WithDropIndex(true), + entmigrate.WithForeignKeys(true), + ); err != nil { + return fmt.Errorf("apply Ent schema: %w", err) + } + + namespaceSQL, err := namespacefks.Generate(namespacefks.GenerateInput{ + Tables: entmigrate.Tables, + ChildTables: input.NamespaceChildTables, + }) + if err != nil { + return fmt.Errorf("generate namespace foreign keys: %w", err) + } + if _, err := database.ExecContext(ctx, string(namespaceSQL)); err != nil { + return fmt.Errorf("apply namespace foreign keys: %w", err) + } + + viewsSQL, err := viewgen.GenerateRecreateSQL(input.EntSchemaPath) + if err != nil { + return fmt.Errorf("generate Ent views: %w", err) + } + if _, err := database.ExecContext(ctx, string(viewsSQL)); err != nil { + return fmt.Errorf("recreate Ent views: %w", err) + } + + return nil +} + +func RenderSQL(plan pgdiff.Plan, validated bool) []byte { + var output bytes.Buffer + output.WriteString("-- Generated by the pg-schema-diff proof of concept. Review before use.\n") + if validated { + output.WriteString("-- Replay validation: passed.\n") + } else { + output.WriteString("-- Replay validation: disabled.\n") + } + fmt.Fprintf(&output, "-- Current schema hash: %s\n", plan.CurrentSchemaHash) + if len(plan.Statements) == 0 { + output.WriteString("-- Schema matches the desired state.\n") + return output.Bytes() + } + + for index, statement := range plan.Statements { + output.WriteString("\n/*\n") + fmt.Fprintf(&output, "Statement %d\n", index) + for _, hazard := range statement.Hazards { + fmt.Fprintf(&output, " - %s: %s\n", hazard.Type, hazard.Message) + } + output.WriteString("*/\n") + fmt.Fprintf(&output, "SET SESSION statement_timeout = %d;\n", statement.Timeout.Milliseconds()) + fmt.Fprintf(&output, "SET SESSION lock_timeout = %d;\n", statement.LockTimeout.Milliseconds()) + ddl := publicSchemaQualifier.Replace(statement.DDL) + output.WriteString(strings.TrimSuffix(strings.TrimSpace(ddl), ";")) + output.WriteString(";\n") + } + + return output.Bytes() +} diff --git a/tools/migrate/pgschemadiff/pgschemadiff_test.go b/tools/migrate/pgschemadiff/pgschemadiff_test.go new file mode 100644 index 0000000000..c78555f5b5 --- /dev/null +++ b/tools/migrate/pgschemadiff/pgschemadiff_test.go @@ -0,0 +1,81 @@ +package pgschemadiff + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/stripe/pg-schema-diff/pkg/diff" +) + +func TestDatabaseURL(t *testing.T) { + // given: + // - a development database URL with credentials and connection parameters + // when: + // - a URL is created for a disposable database + // then: + // - only the database name changes + actual, err := databaseURL( + "postgres://user:p%40ss@localhost:5432/postgres?sslmode=disable&search_path=public", + "pgschemadiff_tmp_example", + ) + require.NoError(t, err) + require.Equal( + t, + "postgres://user:p%40ss@localhost:5432/pgschemadiff_tmp_example?sslmode=disable&search_path=public", + actual, + ) +} + +func TestRenderSQL(t *testing.T) { + // given: + // - a pg-schema-diff plan with timeout metadata and a migration hazard + // when: + // - the plan is rendered as a reviewable migration + // then: + // - the SQL preserves execution settings, hazard context, and statement order + plan := diff.Plan{ + CurrentSchemaHash: "abc123", + Statements: []diff.Statement{ + { + DDL: `ALTER TABLE "public"."billing_profiles" ADD CONSTRAINT "guard" FOREIGN KEY ("namespace", "tax_app_id") REFERENCES public."apps" ("namespace", "id")`, + Timeout: 2 * time.Minute, + LockTimeout: 3 * time.Second, + Hazards: []diff.MigrationHazard{ + {Type: diff.MigrationHazardTypeAcquiresShareRowExclusiveLock, Message: "validating the foreign key takes a lock"}, + }, + }, + }, + } + + actual := string(RenderSQL(plan, true)) + + require.Equal(t, `-- Generated by the pg-schema-diff proof of concept. Review before use. +-- Replay validation: passed. +-- Current schema hash: abc123 + +/* +Statement 0 + - ACQUIRES_SHARE_ROW_EXCLUSIVE_LOCK: validating the foreign key takes a lock +*/ +SET SESSION statement_timeout = 120000; +SET SESSION lock_timeout = 3000; +ALTER TABLE "billing_profiles" ADD CONSTRAINT "guard" FOREIGN KEY ("namespace", "tax_app_id") REFERENCES "apps" ("namespace", "id"); +`, actual) +} + +func TestRenderSQLWithoutChanges(t *testing.T) { + // given: + // - an empty pg-schema-diff plan + // when: + // - the plan is rendered + // then: + // - the output clearly reports that the schema is already current + actual := string(RenderSQL(diff.Plan{CurrentSchemaHash: "abc123"}, false)) + + require.Equal(t, `-- Generated by the pg-schema-diff proof of concept. Review before use. +-- Replay validation: disabled. +-- Current schema hash: abc123 +-- Schema matches the desired state. +`, actual) +} diff --git a/tools/migrate/schema/namespace_foreign_keys.sql b/tools/migrate/schema/namespace_foreign_keys.sql new file mode 100644 index 0000000000..be6f0998fa --- /dev/null +++ b/tools/migrate/schema/namespace_foreign_keys.sql @@ -0,0 +1,111 @@ +-- Code generated by make generate-namespace-fks; DO NOT EDIT. +-- The original Ent foreign keys remain responsible for relationship lifecycle actions. +-- These additive constraints only enforce namespace equality. +-- Child tables: billing_customer_overrides, billing_invoices, billing_profiles. + +-- Required by namespace guard foreign keys referencing "billing_workflow_configs". +CREATE UNIQUE INDEX "billing_workflow_configs_namespace_id_key" ON "billing_workflow_configs" ("namespace", "id"); + +-- Extends Ent foreign key "billing_customer_overrides_billing_profiles_billing_customer_override" with namespace equality. +ALTER TABLE "billing_customer_overrides" + ADD CONSTRAINT "billing_customer_overrides_billing_profile_namespace_guard_fk" + FOREIGN KEY ("namespace", "billing_profile_id") + REFERENCES "billing_profiles" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_customer_overrides_customers_billing_customer_override" with namespace equality. +ALTER TABLE "billing_customer_overrides" + ADD CONSTRAINT "billing_customer_overrides_customer_namespace_guard_fk" + FOREIGN KEY ("namespace", "customer_id") + REFERENCES "customers" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_customer_overrides_tax_codes_billing_customer_overrides" with namespace equality. +ALTER TABLE "billing_customer_overrides" + ADD CONSTRAINT "billing_customer_overrides_tax_code_namespace_guard_fk" + FOREIGN KEY ("namespace", "tax_code_id") + REFERENCES "tax_codes" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_invoices_customers_billing_invoice" with namespace equality. +ALTER TABLE "billing_invoices" + ADD CONSTRAINT "billing_invoices_customer_namespace_guard_fk" + FOREIGN KEY ("namespace", "customer_id") + REFERENCES "customers" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_invoices_apps_billing_invoice_invoicing_app" with namespace equality. +ALTER TABLE "billing_invoices" + ADD CONSTRAINT "billing_invoices_invoicing_app_namespace_guard_fk" + FOREIGN KEY ("namespace", "invoicing_app_id") + REFERENCES "apps" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_invoices_apps_billing_invoice_payment_app" with namespace equality. +ALTER TABLE "billing_invoices" + ADD CONSTRAINT "billing_invoices_payment_app_namespace_guard_fk" + FOREIGN KEY ("namespace", "payment_app_id") + REFERENCES "apps" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_invoices_billing_profiles_billing_invoices" with namespace equality. +ALTER TABLE "billing_invoices" + ADD CONSTRAINT "billing_invoices_source_billing_profile_namespace_guard_fk" + FOREIGN KEY ("namespace", "source_billing_profile_id") + REFERENCES "billing_profiles" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_invoices_apps_billing_invoice_tax_app" with namespace equality. +ALTER TABLE "billing_invoices" + ADD CONSTRAINT "billing_invoices_tax_app_namespace_guard_fk" + FOREIGN KEY ("namespace", "tax_app_id") + REFERENCES "apps" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_invoices_billing_workflow_configs_billing_invoices" with namespace equality. +ALTER TABLE "billing_invoices" + ADD CONSTRAINT "billing_invoices_workflow_config_namespace_guard_fk" + FOREIGN KEY ("namespace", "workflow_config_id") + REFERENCES "billing_workflow_configs" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_profiles_apps_billing_profile_invoicing_app" with namespace equality. +ALTER TABLE "billing_profiles" + ADD CONSTRAINT "billing_profiles_invoicing_app_namespace_guard_fk" + FOREIGN KEY ("namespace", "invoicing_app_id") + REFERENCES "apps" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_profiles_apps_billing_profile_payment_app" with namespace equality. +ALTER TABLE "billing_profiles" + ADD CONSTRAINT "billing_profiles_payment_app_namespace_guard_fk" + FOREIGN KEY ("namespace", "payment_app_id") + REFERENCES "apps" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_profiles_apps_billing_profile_tax_app" with namespace equality. +ALTER TABLE "billing_profiles" + ADD CONSTRAINT "billing_profiles_tax_app_namespace_guard_fk" + FOREIGN KEY ("namespace", "tax_app_id") + REFERENCES "apps" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; + +-- Extends Ent foreign key "billing_profiles_billing_workflow_configs_billing_profile" with namespace equality. +ALTER TABLE "billing_profiles" + ADD CONSTRAINT "billing_profiles_workflow_config_namespace_guard_fk" + FOREIGN KEY ("namespace", "workflow_config_id") + REFERENCES "billing_workflow_configs" ("namespace", "id") + ON UPDATE NO ACTION + ON DELETE NO ACTION; diff --git a/tools/migrate/viewgen/viewgen.go b/tools/migrate/viewgen/viewgen.go index a024299761..c75352901e 100644 --- a/tools/migrate/viewgen/viewgen.go +++ b/tools/migrate/viewgen/viewgen.go @@ -25,6 +25,26 @@ type ViewDef struct { } func GenerateSQL(schemaPath string) ([]byte, error) { + views, err := loadViews(schemaPath) + if err != nil { + return nil, err + } + + return RenderSQL(views), nil +} + +// GenerateRecreateSQL renders the destructive replacement needed to reconcile +// Ent-managed views in a disposable desired-state database. +func GenerateRecreateSQL(schemaPath string) ([]byte, error) { + views, err := loadViews(schemaPath) + if err != nil { + return nil, err + } + + return RenderRecreateSQL(views), nil +} + +func loadViews(schemaPath string) ([]ViewDef, error) { spec, err := (&load.Config{Path: schemaPath}).Load() if err != nil { return nil, fmt.Errorf("load ent schema: %w", err) @@ -35,7 +55,7 @@ func GenerateSQL(schemaPath string) ([]byte, error) { return nil, fmt.Errorf("collect views: %w", err) } - return RenderSQL(views), nil + return views, nil } func GenerateFile(schemaPath, outPath string) error { @@ -104,14 +124,31 @@ func RenderSQL(views []ViewDef) []byte { var buf bytes.Buffer buf.WriteString("-- Code generated by viewgen, DO NOT EDIT.\n") buf.WriteString("-- Regenerate with: make generate-view-sql\n") + renderCreateViews(&buf, views) + return buf.Bytes() +} + +func RenderRecreateSQL(views []ViewDef) []byte { + var buf bytes.Buffer + buf.WriteString("-- Recreate Ent-managed views in a disposable desired-state database.\n") + for i := len(views) - 1; i >= 0; i-- { + fmt.Fprintf(&buf, "DROP VIEW IF EXISTS %q;\n", views[i].Name) + } + if len(views) > 0 { + buf.WriteByte('\n') + } + renderCreateViews(&buf, views) + return buf.Bytes() +} + +func renderCreateViews(buf *bytes.Buffer, views []ViewDef) { for i, view := range views { if i > 0 { buf.WriteString("\n\n") } - fmt.Fprintf(&buf, "CREATE VIEW %q AS\n%s;", view.Name, view.Query) + fmt.Fprintf(buf, "CREATE VIEW %q AS\n%s;", view.Name, view.Query) } buf.WriteByte('\n') - return buf.Bytes() } func entSQLAnnotation(annotations map[string]any) (*entsql.Annotation, error) { diff --git a/tools/migrate/viewgen/viewgen_test.go b/tools/migrate/viewgen/viewgen_test.go new file mode 100644 index 0000000000..b753512767 --- /dev/null +++ b/tools/migrate/viewgen/viewgen_test.go @@ -0,0 +1,33 @@ +package viewgen + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRenderRecreateSQL(t *testing.T) { + // given: + // - two Ent-managed views in creation order + // when: + // - replacement SQL is rendered + // then: + // - existing views are dropped in reverse order before both are recreated + views := []ViewDef{ + {Name: "first_view", Query: "SELECT 1 AS value"}, + {Name: "second_view", Query: "SELECT value FROM first_view"}, + } + + actual := string(RenderRecreateSQL(views)) + + require.Equal(t, `-- Recreate Ent-managed views in a disposable desired-state database. +DROP VIEW IF EXISTS "second_view"; +DROP VIEW IF EXISTS "first_view"; + +CREATE VIEW "first_view" AS +SELECT 1 AS value; + +CREATE VIEW "second_view" AS +SELECT value FROM first_view; +`, actual) +}