Skip to content

fix: harden validate-upgrade preflight checks#10

Merged
intel352 merged 1 commit into
mainfrom
fix/validate-upgrade-hardening
Apr 27, 2026
Merged

fix: harden validate-upgrade preflight checks#10
intel352 merged 1 commit into
mainfrom
fix/validate-upgrade-hardening

Conversation

@intel352
Copy link
Copy Markdown
Contributor

Summary

  • reject non-empty schemas before validate-upgrade applies baseline migrations
  • compare migration versions without lossy numeric parsing and ignore down-only files for baseline continuity
  • document the empty-schema requirement in command help and add hermetic fake-driver success coverage

Verification

  • go test ./pkg/cli -run 'TestValidateUpgrade|TestRootIncludesValidateUpgradeCommand' -count=1\n- go test ./... -count=1\n- go build ./cmd/workflow-migrate\n- ./workflow-migrate validate-upgrade --help\n- git diff --check\n\n## Notes\n\nThis follows up PR feat: validate migration upgrade paths #9 reviewer findings: a database with user objects but no migration metadata could be accepted as the baseline target, down-only files could satisfy baseline continuity, and version matching used numeric coercion.

Copilot AI review requested due to automatic review settings April 27, 2026 14:25
@intel352 intel352 merged commit 8ebc495 into main Apr 27, 2026
9 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens the validate-upgrade CLI preflight behavior to better ensure baseline/candidate upgrade validation runs only against a truly clean starting schema and performs baseline-version continuity checks more reliably.

Changes:

  • Add an initial “empty schema” preflight check before running baseline migrations, and document the requirement in validate-upgrade --help.
  • Fix baseline-version continuity checks by avoiding lossy numeric parsing and ignoring .down.sql files for baseline presence.
  • Expand test coverage with a hermetic fake-driver success case and an integration test that rejects non-empty schemas without migration metadata.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
pkg/cli/validate_upgrade.go Adds schema preflight check; improves version matching and ignores down-only files for baseline continuity.
pkg/cli/validate_upgrade_test.go Updates unit tests to bypass schema check for fake DSNs; adds down-only baseline rejection + success coverage.
pkg/cli/root_test.go Adds integration coverage ensuring validate-upgrade rejects non-empty schemas even without migration metadata.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

return validateUpgradeWithSchemaCheck(ctx, d, baselineReq, candidateReq, ensureEmptySchema)
}

func validateUpgradeWithSchemaCheck(ctx context.Context, d interfaces.MigrationDriver, baselineReq, candidateReq interfaces.MigrationRequest, checkSchema func(context.Context, string) error) (upgradeValidationResult, error) {
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateUpgradeWithSchemaCheck assumes baseline and candidate migrations run against the same database, but it only runs checkSchema on baselineReq.DSN and never verifies that baselineReq.DSN == candidateReq.DSN. If these differ (e.g. via programmatic use), the function would validate/upgrade two different databases and still report success/failure as if it were a single upgrade path. Consider explicitly rejecting mismatched DSNs (and/or running the schema check against the DSN that will actually be migrated).

Suggested change
func validateUpgradeWithSchemaCheck(ctx context.Context, d interfaces.MigrationDriver, baselineReq, candidateReq interfaces.MigrationRequest, checkSchema func(context.Context, string) error) (upgradeValidationResult, error) {
func validateUpgradeWithSchemaCheck(ctx context.Context, d interfaces.MigrationDriver, baselineReq, candidateReq interfaces.MigrationRequest, checkSchema func(context.Context, string) error) (upgradeValidationResult, error) {
if baselineReq.DSN != candidateReq.DSN {
return upgradeValidationResult{}, fmt.Errorf("validate-upgrade requires baseline and candidate to target the same database")
}

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +137
SELECT count(*)
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = current_schema()
AND c.relkind IN ('r', 'p', 'v', 'm', 'S', 'f')
AND c.relname NOT IN ('schema_migrations', 'goose_db_version', 'atlas_schema_revisions')
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensureEmptySchema currently counts only relation-like objects from pg_class (tables/partitions/views/materialized views/sequences/foreign tables). Schemas that contain other user-defined objects (e.g., functions in pg_proc, types/domains in pg_type, extensions, etc.) will still pass the “empty schema” check even though the schema is not truly empty. If the intent is to reject any non-empty schema, the check should be broadened (or the help/error text should be narrowed to match exactly what is being checked).

Suggested change
SELECT count(*)
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = current_schema()
AND c.relkind IN ('r', 'p', 'v', 'm', 'S', 'f')
AND c.relname NOT IN ('schema_migrations', 'goose_db_version', 'atlas_schema_revisions')
WITH object_counts AS (
SELECT count(*) AS cnt
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = current_schema()
AND c.relkind IN ('r', 'p', 'v', 'm', 'S', 'f')
AND c.relname NOT IN ('schema_migrations', 'goose_db_version', 'atlas_schema_revisions')
UNION ALL
SELECT count(*) AS cnt
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = current_schema()
UNION ALL
SELECT count(*) AS cnt
FROM pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = current_schema()
AND t.typtype IN ('c', 'd', 'e', 'm', 'r')
AND t.typrelid = 0
UNION ALL
SELECT count(*) AS cnt
FROM pg_extension e
JOIN pg_namespace n ON n.oid = e.extnamespace
WHERE n.nspname = current_schema()
)
SELECT COALESCE(sum(cnt), 0)
FROM object_counts

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants