feat: Add pgschema.toml configuration file support#433
Open
NFUChen wants to merge 39 commits into
Open
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… flags Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a config file defines [schemas] with a SQL query, plan and apply commands discover tenant schemas dynamically and iterate over each one. Dump is excluded since it produces a single template schema. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests cover: no config file, explicit config path, env overrides with inheritance, schemas section, plan fields, boolean overrides, and command-level config fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ulti-tenant schema handling
feat: Add `pgschema.toml` configuration file support
…p, and plan commands
Greptile SummaryThis PR adds TOML configuration support for pgschema commands. The main changes are:
Confidence Score: 3/5This is close, but the structured multi-schema output path should be fixed before merging.
cmd/plan/plan.go Important Files Changed
Reviews (2): Last reviewed commit: "test: add tests for deriveSchemaOutputTa..." | Re-trigger Greptile |
- Renamed `GeneratePlan` to `GenerateSchemaPlan` for clarity. - Updated `runPlan` and `runPlanMultiSchema` to use the new schema plan generation function. - Consolidated the `MultiPlan` and `Plan` structures into a unified `Plan` structure that handles both single and multi-schema operations. - Adjusted methods to work with the new `Plan` structure, including `AddSchema`, `HasAnyChanges`, `ToJSON`, and `ToSQL`. - Updated tests to reflect the changes in the plan structure and ensure proper functionality. - Enhanced JSON serialization and deserialization for the new plan structure.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add
pgschema.tomlconfiguration file supportSummary
Adds support for a TOML-based configuration file (
pgschema.toml) so users can define connection parameters, flags, and per-environment overrides without repeating CLI flags on every invocation.Before:
After:
# with pgschema.toml in the working directory pgschema plan pgschema applyFeatures
1. Flat config file
All existing CLI flags can be set in
pgschema.toml:2. Named environments (
--env)[env.*]blocks define per-environment overrides that inherit from the base level:Environment merging uses TOML metadata (
IsDefined) so explicitly setting a boolean tofalsein an env block correctly overrides a base-leveltrue— zero values are not silently skipped.3. Multi-tenant schema loop (
[schemas])For multi-tenant setups, a
[schemas]block with a SQL query discovers schema names at runtime.planandapplyiterate over all discovered schemas automatically:The discovery query runs inside a read-only transaction to prevent accidental data modification (CREATE/DROP/INSERT are rejected by Postgres).
4. Precedence
CLI flags always win: CLI flags > env vars > config env block > config base > defaults.
Config values are applied in
PreRunEhooks before env var resolution, so existingPGHOST/PGPORT/etc. behavior is preserved. A flag is only populated from config ifcmd.Flags().Changed(flag)returnsfalse.Global flags added
--configpgschema.toml--env--configto a missing file → exit with error.pgschema.tomlmissing → silently proceed (backward compatible).--envwithout a config file → exit with error.Files changed
Code
cmd/config/config.goIsDefined,DiscoverSchemas(read-only tx, URL-encoded DSN),Get()/SetResolved()singletoncmd/config/config_test.gocmd/config_integration_test.gocmd/root.go--config/--envglobal flags,loadConfig()inPersistentPreRuncmd/plan/plan.goapplyConfigToPlan()PreRunE,runPlanMultiSchema()using top-levelPlanfor combined output, removedMarkFlagRequired("file"), unifiedprocessOutput()cmd/plan/output_test.goTestDeriveSchemaOutputTargetremoved since per-schema-file logic was dropped in favor of single combined output)cmd/apply/apply.goapplyConfigToApply()PreRunE,runApplyMultiSchema(),applyPlanFile()iterating overPlan.Schemasin sorted order with auto-detection of single vs multi-schema plan filescmd/apply/apply_test.goTestRunApply_PlanFlagSkipsMultiSchemaensures--planshort-circuits the multi-schema path even when[schemas]is configuredcmd/apply/apply_integration_test.goGeneratePlan→GenerateSchemaPlan; plan files now go throughPlan.AddSchema("public", ...)and are read back viaSchemas["public"]cmd/dump/dump.goapplyConfigToDump()PreRunEcmd/{ignore,migrate}_integration_test.go,cmd/plan/external_db_integration_test.goGenerateSchemaPlanand wrap withplan.NewPlan().AddSchema(...)for outputinternal/plan/plan.goPlanis now a top-level container:{ version, pgschema_version, created_at, schemas: map[string]*SchemaPlan }. AddsNewPlan(),AddSchema(),SortedSchemaNames(),SummaryString(), multi-schema-awareHumanColored()/ToSQL()(single-schema renders without header), andFromJSON()for the new shapeinternal/plan/schema_plan.goSchemaPlantype holds the per-schemaGroups,SourceFingerprint,SourceDiffs, plus all the previousPlanrendering logic (HumanColored,ToSQL,calculateSummaryFromSteps, table/view/materialized-view detail writers, helpers). All extracted verbatim from the oldplan.go.internal/plan/schema_plan_test.goSchemaPlansummary/no-changes, JSON round-trip acrosstestdata/diff/migrate/v*(now via top-levelPlan), debug JSON round-trip withSourceDiffs, single-schema header omissioninternal/plan/plan_test.goPlanAPI:AddSchema,SortedSchemaNames,ToJSON/FromJSONround-trip withschemaskey,SchemaEntry_ExcludesTopLevelFields,HumanColored_MultiSchema,ToSQL_MultiSchema,SummaryString,CreatedAt_UsesTestTimeTest data — regenerated in this revision
All ~180
testdata/diff/**/plan.jsongolden files were regenerated to match the new top-level JSON shape. The change is purely structural — no SQL, fingerprints, operations, paths, or step ordering were modified.Before (single-schema flat):
{ "version": "1.0.0", "pgschema_version": "1.9.0", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "..." }, "groups": [ { "steps": [ ... ] } ] }After (schema-keyed):
{ "version": "1.0.0", "pgschema_version": "1.9.0", "created_at": "1970-01-01T00:00:00Z", "schemas": { "public": { "source_fingerprint": { "hash": "..." }, "groups": [ { "steps": [ ... ] } ] } } }Why every file changed:
Planis now always the multi-schema container;groupsandsource_fingerprintmoved insideschemas.<name>.version,pgschema_version, andcreated_atare preserved; per-schema entries deliberately omit them (verified byTestPlan_SchemaEntry_ExcludesTopLevelFields).The diff per file is mechanical (added wrapping
"schemas": { "public": { ... } }, plus 2-space indentation shift), which is why the diffstat shows ~180 files with ±5–6k lines but no semantic changes:with
internal/plan/plan.goshrinking by ~1100 lines as logic moved intointernal/plan/schema_plan.go.Misc
README.md— documentation for config file, named environments, multi-schema loop, and precedence.Design decisions
BurntSushi/tomlfor ignore config — no new dependency.config.Get()): Config is loaded once inPersistentPreRunand read by subcommands viaconfig.Get(). Matches the existing pattern where global vars are set inPreRunEhooks.--fileis no longerMarkFlagRequired: When config providesfile, requiring--fileon the CLI would defeat the purpose. Validation moved to runtime inrunPlan("--file is required (provide via flag, config file, or environment)").[schemas].queryis user-provided SQL executed against the target database. Wrapping inBeginTx(ctx, &sql.TxOptions{ReadOnly: true})prevents accidentalCREATE/DROP/INSERTeven if the query is malformed or malicious. Verified byTestDiscoverSchemas_ReadOnlyEnforcement.DiscoverSchemas: Built vianet/urlto avoid injection through host/user/password fields.PlanJSON format ({"schemas": {"tenant_1": {...}, "tenant_2": {...}}}).apply --planiterates schemas in sorted order. This eliminates the previous limitation of needing one plan file per tenant.Plan.HumanColored()andPlan.ToSQL()detectlen(Schemas) == 1and delegate directly to the underlyingSchemaPlan, so single-schema CLI output is unchanged from before.--planshort-circuits multi-schema: When--planis provided,RunApplyskips the[schemas]discovery path even if config has it set, since the plan file itself dictates which schemas to apply. Covered byTestRunApply_PlanFlagSkipsMultiSchema.Backward compatibility
pgschema.toml, command behavior is identical to before — except plan JSON output now wraps under"schemas". This is a breaking change for anyone parsing plan JSON externally or replaying plan files produced by older pgschema versions. All golden plan files intestdata/diff/**/plan.jsonwere regenerated accordingly.Flow diagrams
End-to-end command flow
flowchart TD A[Start command plan/apply/dump] --> B[PersistentPreRun loadConfig] B --> C{Config file exists?} C -->|No and --config explicit| C1[Exit with error] C -->|No and --env set| C2[Exit with error] C -->|No default file| C3[Continue with nil config] C -->|Yes| D[Parse TOML base + env] D --> E[Set global resolved config] E --> F[Subcommand PreRunE applyConfigToX] F --> G[Apply config values only when flag not changed] G --> H[Apply env vars and connection defaults] H --> I{Has schemas.query and schema not explicitly set?} I -->|No| J[Single-schema flow] I -->|Yes| K[Discover schemas in read-only transaction] K --> L{Command type} L -->|plan| M[Loop schemas, GenerateSchemaPlan, AddSchema to combined Plan] L -->|apply --file| N[Loop schemas, GenerateSchemaPlan + ApplyMigration each] L -->|apply --plan| N2[Load Plan from file, iterate Schemas in sorted order] M --> M1[Write combined Plan JSON/SQL/Human to single output] M1 --> M2[Progress logs to stderr] N --> N3[Progress logs to stderr] N2 --> N4[Apply each SchemaPlan with its schema name] J --> O[Single-schema behavior preserved, no header in output] M2 --> P[Done] N3 --> P N4 --> P O --> PPrecedence and output behavior
Precedence order
[env.<name>])pgschema.tomlMulti-schema plan output
All schemas are combined into a single output. JSON wraps per-schema plans in a
"schemas"map:--output-json plan.jsonPlanJSON file with all schemas--output-json stdoutPlanJSON printed to stdout--output-human── Schema: <name> ──headers (single-schema: no header)--output-sql-- Schema: <name>comment headers (single-schema: no header)Plan JSON format
{ "version": "1.0.0", "pgschema_version": "1.9.0", "created_at": "2025-01-01T00:00:00Z", "schemas": { "tenant_1": { "source_fingerprint": { "hash": "..." }, "groups": [...] }, "tenant_2": { "source_fingerprint": { "hash": "..." }, "groups": [...] } } }apply --plan combined.jsoniteratesschemasin sorted order and applies eachSchemaPlanagainst its named schema.