From 9682a285afd4f166166586066e462e496efd915b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 14:59:44 +0000 Subject: [PATCH] d1 import: fix Postgres SSL default, D1 metadata table, verify errors - Default sslrootcert=system for the destination Postgres role so verify-full mode doesn't fall back to a missing ~/.postgresql/root.crt. - Exclude Cloudflare's _cf_METADATA table using the existing ORM/metadata table exclusion mechanism, so it's skipped during schema conversion, data load, and verify like other internal bookkeeping tables. - Wrap the row-fetch error in postgresRowSignature with the table and primary key context, matching tableFingerprintFromPostgres. - Backfill opts.InputPath from saved migration state when --sqlite is passed explicitly, since Verify() needs InputPath regardless. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MjeJgHVQNc5nQc15Yb9dNB --- internal/import/d1/import.go | 3 ++- internal/import/d1/orm_metadata.go | 6 +++++ internal/import/d1/orm_metadata_test.go | 1 + internal/import/d1/verify.go | 7 +++++ internal/import/d1/verify_checks.go | 2 +- internal/import/d1/verify_test.go | 34 +++++++++++++++++++++++++ 6 files changed, 51 insertions(+), 2 deletions(-) diff --git a/internal/import/d1/import.go b/internal/import/d1/import.go index a3a5214d1..973b7c03d 100644 --- a/internal/import/d1/import.go +++ b/internal/import/d1/import.go @@ -449,7 +449,8 @@ func ResolveDestURI(ctx context.Context, psClient *ps.Client, opts ImportOptions Password: role.Role.Password, Database: dbName, SSLMode: "verify-full", - Options: map[string]string{}, + // libpq falls back to ~/.postgresql/root.crt without this, which most machines lack. + Options: map[string]string{"sslrootcert": "system"}, }) return uri, func() error { return role.Cleanup(ctx, "postgres") }, nil diff --git a/internal/import/d1/orm_metadata.go b/internal/import/d1/orm_metadata.go index 42df451cc..aae468b7e 100644 --- a/internal/import/d1/orm_metadata.go +++ b/internal/import/d1/orm_metadata.go @@ -82,6 +82,12 @@ var ormMetadataRules = []ormMetadataRule{ remediation: "After import, re-baseline Goose version table on Postgres; goose_db_version from SQLite is not portable", match: matchTableName("goose_db_version"), }, + { + code: "CLOUDFLARE_D1_METADATA", + orm: "Cloudflare D1", + remediation: "_cf_METADATA is Cloudflare D1-internal bookkeeping and is not imported into Postgres", + match: matchTableName("_cf_METADATA"), + }, } func matchTableName(name string) func(string) bool { diff --git a/internal/import/d1/orm_metadata_test.go b/internal/import/d1/orm_metadata_test.go index e9496fac7..ac5bb6482 100644 --- a/internal/import/d1/orm_metadata_test.go +++ b/internal/import/d1/orm_metadata_test.go @@ -22,6 +22,7 @@ func TestIsORMMetadataTable(t *testing.T) { {"alembic_version", true, "ALEMBIC_VERSION"}, {"typeorm_metadata", true, "TYPEORM_METADATA"}, {"goose_db_version", true, "GOOSE_MIGRATIONS"}, + {"_cf_METADATA", true, "CLOUDFLARE_D1_METADATA"}, {"users", false, ""}, {"migrations", false, ""}, {"organizations", false, ""}, diff --git a/internal/import/d1/verify.go b/internal/import/d1/verify.go index 367b132b3..b674cc3b0 100644 --- a/internal/import/d1/verify.go +++ b/internal/import/d1/verify.go @@ -302,6 +302,13 @@ func ResolveVerifyDBName(opts VerifyOptions, dbNameExplicit bool) string { func resolveVerifySQLitePath(opts VerifyOptions) (VerifyOptions, string, error) { if opts.SQLitePath != "" { + if opts.InputPath == "" && opts.MigrationID != "" { + state, err := LoadState(opts.Org, opts.Database, opts.Branch, opts.MigrationID) + if err != nil { + return opts, "", err + } + opts.InputPath = state.InputPath + } return opts, opts.SQLitePath, nil } diff --git a/internal/import/d1/verify_checks.go b/internal/import/d1/verify_checks.go index 4b91ab1a6..bdc5072af 100644 --- a/internal/import/d1/verify_checks.go +++ b/internal/import/d1/verify_checks.go @@ -714,7 +714,7 @@ func postgresRowSignature(ctx context.Context, db *sql.DB, table TableSchema, pk ) var sig sql.NullString if err := db.QueryRowContext(ctx, query, pkVal).Scan(&sig); err != nil { - return "", err + return "", fmt.Errorf("postgres row signature %s (%s=%s): %w", table.Name, pkCol, pkVal, err) } if !sig.Valid { return "", fmt.Errorf("row not found in %s where %s = %s", table.Name, pkCol, pkVal) diff --git a/internal/import/d1/verify_test.go b/internal/import/d1/verify_test.go index 871268ccb..2277f8ede 100644 --- a/internal/import/d1/verify_test.go +++ b/internal/import/d1/verify_test.go @@ -98,6 +98,40 @@ func TestResolveVerifySQLitePathDefaultsFromInput(t *testing.T) { } } +func TestResolveVerifySQLitePathBackfillsInputPathWhenSQLiteExplicit(t *testing.T) { + t.Setenv("PSCALE_TEST_MODE", "1") + + org, database, branch := "acme", "mydb", "main" + migrationID := "verify004" + input := testFixture(t) + if err := SavePlan(&PlanResult{ + MigrationID: migrationID, + Org: org, + Database: database, + Branch: branch, + InputPath: input, + }); err != nil { + t.Fatalf("SavePlan: %v", err) + } + + gotOpts, sqlitePath, err := resolveVerifySQLitePath(VerifyOptions{ + Org: org, + Database: database, + Branch: branch, + MigrationID: migrationID, + SQLitePath: "/nonexistent/staging.sqlite", + }) + if err != nil { + t.Fatalf("resolveVerifySQLitePath: %v", err) + } + if sqlitePath != "/nonexistent/staging.sqlite" { + t.Fatalf("sqlite path = %q, want explicit --sqlite path", sqlitePath) + } + if gotOpts.InputPath != input { + t.Fatalf("InputPath = %q, want backfilled %q", gotOpts.InputPath, input) + } +} + func TestResolveVerifySQLitePathFailsOnBadMigrationIDWithInput(t *testing.T) { t.Setenv("PSCALE_TEST_MODE", "1")