You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
chore: add export-db-dump.sh, cover extension_version_change in the dump - #2258
scrub-db-dump.sh and import-db-dump.sh already agree on a per-table Postgres COPY-TEXT dump format, but nothing in the repo actually produces that dump from a live source - someone has to hand-roll the \copy ... to commands themselves. Ran into this while testing #2246 against a production DB copy.
scripts/export-db-dump.sh (new): exports the 9 tables import-db-dump.sh expects from a given Postgres connection string, all in one transaction/snapshot (REPEATABLE READ, READ ONLY) so concurrent writes on the source can't leave the tables inconsistent with each other. Stages into a temp directory and promotes each table's file individually once psql has committed - deliberately per-file rather than a directory-level swap, since scrub-db-dump.sh leaves *.csv.bak originals alongside the redacted files and a whole-directory swap would silently delete those (and anything else already there) on a routine re-export. This isn't atomic as a set: a failure partway through the promotion loop can leave a mixed generation, accepted as the better tradeoff over destroying files the export doesn't own (a stale CSV is fixed by re-running the export). Also checks for psql up front, restricts permissions on the raw (pre-redaction) dump, resolves symlinked output directories to their real path, and runs psql -X so a ~/.psqlrc can't override its error handling.
scripts/import-db-dump.sh: adds extension_version_change (introduced by V1_71) to the table list. It predates that table, so a DB copy built via this pipeline had an always-empty extension_version_change regardless of production reality - nothing that reads the registry changes feed (/api/-/version-changes, and anything fix: report a namespace rename on the registry changes feed #2246 touches) could be exercised against such a copy. Its sequence reset also falls back to the <table>_seq naming convention when pg_get_serial_sequence can't resolve a sequence (true for extension_version_change on any schema before fix: report a namespace rename on the registry changes feed #2246's ownership fix, including a fresh target pinned to an older dump version).
Depends on #2246 for full correctness: extension_version_change_seq (V1_71) was created without OWNED BY, unlike every other entity's sequence, which means pg_get_serial_sequence can't find it on an unpatched schema. #2246's V1_77 now fixes that (folded in there instead of shipping a second, colliding V1_77 here). The naming-convention fallback above means this still works correctly regardless of merge order, but the underlying schema gap is real either way.
Test plan
bash -n on both scripts.
Per-file publish verified directly against a real filesystem with a psql stub: a pre-existing .csv.bak and an unrelated file both survive an export run untouched, all 9 real tables refresh.
Symlinked output directory and apostrophe-in-path handling verified directly (reproduced each bug first, then confirmed the fix).
detect_actual_cols/version-detection empty-table handling verified against a real empty CSV and a set -e array-assignment repro.
Not run end-to-end here (no Docker available in this environment, which import-db-dump.sh needs for its scratch Postgres/Flyway containers) - worth a real run through export-db-dump.sh → scrub-db-dump.sh → import-db-dump.sh before merging.
scrub-db-dump.sh and import-db-dump.sh already had a well-defined,
matched per-table dump format, but nothing in the repo produced that
dump from a live source in the first place - export-db-dump.sh fills
that gap, taking every table in one transaction/snapshot so concurrent
writes on the source can't leave the tables inconsistent with each
other.
extension_version_change (added by V1_71) was missing from both the
export and the existing import-db-dump.sh entirely, since it predates
that table. Without it, a DB copy built via this pipeline can't
exercise anything version-changes-feed related - the table is simply
empty regardless of what's actually in the app. Added it to both
scripts' table lists; import-db-dump.sh's delete/import ordering,
column adaptation and id-sequence fixup are all already driven
generically off that list, so no other logic changes were needed
there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
The import-db-dump.sh update is a clean, correct one-liner addition with accurate doc updates throughout. The new export-db-dump.sh is well-structured and the REPEATABLE READ + READ ONLY transaction approach correctly pins all \copy calls to a single consistent snapshot. The only gap is a missing psql availability check, which is inconsistent with the other scripts in the same directory.
- export-db-dump.sh: check for psql on PATH up front, matching
scrub-db-dump.sh/import-db-dump.sh, instead of failing on the last
line with an empty output directory left behind.
- export-db-dump.sh: set a restrictive umask before creating anything.
The raw dump holds personal_access_token.value,
signature_key_pair.private_key and user_data.eclipse_token until
scrub-db-dump.sh redacts them, so it shouldn't inherit whatever
umask the caller happens to have.
- export-db-dump.sh: stage \copy output next to the target directory
and only move files into place once psql has committed. \copy
writes its file directly, independent of the transaction, so a
failure partway through previously left the target with a mix of
freshly-overwritten and stale files that still passed
import-db-dump.sh's existence checks.
- import-db-dump.sh: handle a table with zero dump rows during Flyway
version auto-detection instead of crashing. Verified the crash first
(an empty CSV makes counts.most_common(1)[0] raise IndexError, which
set -e propagates through the array assignment and kills the
script), then confirmed the fix handles it: an empty dump file is
now treated as carrying no signal for that table rather than
vetoing every candidate version, and detection is refused with a
clear message only if every table is empty.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolve default dump path relative to the project root
scripts/export-db-dump.sh:12
The default output path is resolved from the caller's working directory, while both scrub-db-dump.sh and import-db-dump.sh resolve their default db/dump under the repository root. Invoking this script by absolute path from another directory therefore writes a dump that the next pipeline step cannot find with its defaults. Anchor only the default path to the script's project root.
…path
import-db-dump.sh: an empty dump file for a table was skipped from
version matching entirely, not just its exact column count - so a
version from before that table existed could still "match" on the
strength of every other table alone (concretely: an empty
extension_version_change.csv could select a pre-V1_71 schema). The
file existing at all means the table existed at export time, so a
candidate version where it has zero columns (doesn't exist yet) is
now rejected outright; only the exact-count comparison is skipped
when there's no row signal for it. Verified both branches directly
against a table_column_count stub.
export-db-dump.sh: the default output directory was relative to the
caller's cwd, unlike scrub-db-dump.sh/import-db-dump.sh's own db/dump
default, which both resolve against the repo root. Running this
script by path from elsewhere silently wrote the dump somewhere
neither of those two scripts would look for it. Anchored the default
to the script's own project root the same way.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Staging protects against a failure during psql, but promoting nine files in a loop is still not atomic as a set. If the process is interrupted or a later mv fails, earlier tables are new while later tables remain old, recreating the mixed dump that staging is intended to prevent; the importer only checks that each file exists and will accept it. Publish a generation atomically (for example via a versioned directory/pointer) or add a manifest that import/scrub validate before consuming the files.
Unowned sequence is not reset after import
scripts/import-db-dump.sh:549
extension_version_change_seq is created as the column default but is never OWNED BY extension_version_change.id (V1_71__ExtensionVersion_Changes_Feed.sql:11-15). Consequently pg_get_serial_sequence returns NULL for this newly included table, so the imported IDs do not advance its sequence and the next feed insert can fail with a duplicate primary key. Explicitly map this unowned sequence (or add ownership in a migration) so it is reset with the others.
… ownership
export-db-dump.sh: publish the staged dump as two directory renames
(old -> .previous, staged -> live) instead of moving each table's
file into place individually. A crash or a failed move partway
through the old per-file loop could still leave the target with some
tables on the new generation and some on the old one - exactly the
mixed state staging was introduced to prevent in the first place.
Verified both the success and failure paths directly: on failure the
target is left completely untouched, on success no staging/backup
directories are left behind.
V1_77__ExtensionVersionChange_Sequence_Ownership.sql: fixes a real,
independently-verified server-side gap this PR's own testing exposed.
extension_version_change_seq (V1_71) was created without OWNED BY,
unlike every other entity's sequence - confirmed by checking
V1_40__Entity_Sequencies.sql, which explicitly added OWNED BY for
every one of import-db-dump.sh's original 8 tables. Without it,
pg_get_serial_sequence('extension_version_change', 'id') returns
NULL, so import-db-dump.sh's existing generic per-table sequence
reset silently skips this table - reproducing the exact duplicate-key
crash hit earlier when this table's sequence wasn't reset by hand,
except now happening through the "fixed" script itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Folded in from #2258 rather than shipping as its own V1_77, which
would collide with this PR's own V1_77 the moment both land - same
table, same migration file is also just a better fit: this PR is the
one already touching extension_version_change's schema semantics.
extension_version_change_seq was created (V1_71) without OWNED BY,
unlike every other entity's sequence (V1_40 added this same link for
the sequences that predated it). Without it, pg_get_serial_sequence
can't find the sequence, which silently defeats
scripts/import-db-dump.sh's generic per-table sequence reset for this
one table - discovered while testing #2258's db-dump tooling changes
against a production copy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Moved the ALTER SEQUENCE fix into #2246's own V1_77 (same table, same
migration file is the better fit anyway) rather than shipping a
second, colliding V1_77 here - Flyway can't apply two migrations at
the same version once both PRs land.
import-db-dump.sh: pg_get_serial_sequence also returns NULL for
extension_version_change on any target schema before that ownership
fix has actually run - notably a fresh target, which this script
deliberately migrates only to the dump's own Flyway version, not the
latest one. Added a fallback to the table's own <table>_seq naming
convention (every table here follows it once it has a sequence) so
the reset works regardless of merge order or which schema version the
target ends up on.
export-db-dump.sh: the two-rename publish wasn't atomic as a pair -
if the second mv failed after the first succeeded, the exit trap
deleted the old generation it had just moved aside instead of
restoring it, leaving no usable output directory at all. Track
whether publication completed and have cleanup restore the previous
directory when it didn't. Verified all three paths (failure before
any rename, failure between the two renames, success) directly:
failure now always leaves a usable directory with the right contents,
success leaves no leftovers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Canonicalize symlinked output directories before staging
scripts/export-db-dump.sh:28
pwd preserves a logical symlink path, so when output-dir is a symlink this later renames the symlink itself and replaces it with a new directory; the real target directory receives no dump. Canonicalize the physical directory before creating the sibling stage so export behaves like scrub/import on symlinked dump locations.
This issue also appears on line 75 of the same file.
Run psql with -X to prevent unsafe user configuration
scripts/export-db-dump.sh:67
Run psql with -X. Otherwise the caller's ~/.psqlrc is loaded after the command-line variable assignment and can change ON_ERROR_STOP or transaction behavior; a COPY error can then leave psql exiting successfully and cause an incomplete stage to be published. The direct-client path already enforces this in scripts/import-db-dump.sh:156-160.
pwd (without -P) preserves a logical symlink path, so when the output
directory is a symlink the rename-based publish renamed the symlink
itself rather than the real directory it points at - the actual
target directory was silently left on the old generation forever,
and a plain directory took the symlink's place. Reproduced directly
(a symlinked "dump-link" ended up replaced by a real directory, with
the real target directory it used to point at orphaned and
untouched), then confirmed pwd -P fixes it: the symlink stays a
symlink and correctly resolves to the new generation.
Also run psql with -X, matching import-db-dump.sh's own target_psql -
without it, a ~/.psqlrc loads after -v ON_ERROR_STOP=1 and could
override it, letting a broken export get published as if it were
complete.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addressed the two "previously missed" findings from the latest review (no standalone threads to reply on):
Symlinked output directories: `pwd` (without `-P`) preserves a logical symlink path, so when the output directory is a symlink, the rename-based publish renamed the symlink itself rather than the real directory it points at - the actual target directory was silently orphaned on the old generation, replaced by a plain directory at the symlink's old path. Reproduced this directly (a symlinked `dump-link` ended up replaced by a real directory; the real target it used to point at survived but became unreachable through it), then confirmed `pwd -P` fixes it - the symlink stays a symlink and correctly resolves to the new generation.
`psql -X`: added, matching `import-db-dump.sh`'s own `target_psql` - without it, a `~/.psqlrc` loads after `-v ON_ERROR_STOP=1` and could override it, letting a broken export get published as if it were complete.
Escape staging paths embedded in generated psql commands
scripts/export-db-dump.sh:73
The generated \copy command embeds the staging path inside a single-quoted psql argument without escaping it. A valid output path (including the default when the checkout path contains an apostrophe) then fails to parse. Run psql from inside the staging directory and use fixed relative filenames so arbitrary filesystem paths never enter the command text.
Publishing by renaming the whole output directory moved everything in
it - not just the 9 generated CSVs - into the backup path the exit
trap then deleted. scrub-db-dump.sh deliberately leaves *.csv.bak
originals alongside the redacted files until they've been reviewed;
a whole-directory swap silently destroyed those (and anything else
already in the directory) the moment this script is re-run to refresh
a dump. Reverted to promoting each of the 9 known files individually,
which can only ever touch filenames this script itself owns. Verified
directly with a psql stub: a pre-existing .csv.bak and an unrelated
file both survive an export run untouched, while all 9 real tables
refresh.
Also stopped embedding STAGE_DIR's full path inside the generated
\copy command's single-quoted argument - that path comes from OUT_DIR
(derived from the repo checkout path or a caller-supplied directory),
and psql has no way to escape a literal "'" in it, so a path
containing one broke the quoting outright. Reproduced this directly
(a literal apostrophe in the path corrupts the generated SQL), then
fixed it by cd-ing into the staging directory and \copy-ing to bare
relative filenames instead, which can never contain a quote since
they're fixed table names under this script's own control. Verified
the generated SQL never contains the path at all, and a full export
run into an apostrophe-containing directory succeeds end to end.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addressed the "previously missed" quoting finding too (no standalone thread): the generated `\copy ... to '${STAGE_DIR}/${t}.csv' ...` embedded STAGE_DIR's full path inside a single-quoted psql argument, with no way to escape a literal `'` in it - a path containing one (plausible: a username, a parent directory name) broke the SQL outright. Reproduced this directly, then fixed it by `cd`-ing into the staging directory and `\copy`-ing to bare relative filenames instead, so no filesystem path ever enters the generated SQL text at all. Verified a full export into an apostrophe-containing directory now succeeds end to end.
Fixed in 036f315 (same commit as the file-preservation fix above).
The comment explained why per-file promotion was chosen over a
directory swap, but not what it gives up: a failure partway through
can leave a mixed generation. Spelled that out next to the loop
itself, not just in the PR description.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Folded in from #2258 rather than shipping as its own V1_77, which
would collide with this PR's own V1_77 the moment both land - same
table, same migration file is also just a better fit: this PR is the
one already touching extension_version_change's schema semantics.
extension_version_change_seq was created (V1_71) without OWNED BY,
unlike every other entity's sequence (V1_40 added this same link for
the sequences that predated it). Without it, pg_get_serial_sequence
can't find the sequence, which silently defeats
scripts/import-db-dump.sh's generic per-table sequence reset for this
one table - discovered while testing #2258's db-dump tooling changes
against a production copy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: report a namespace rename on the registry changes feed
Renaming a namespace moved every extension's versions to the new
namespace row in place, without appending anything to
extension_version_change. The feed kept reporting the old
(namespace, extension, version) tuple as ACTIVE forever, and the new
tuple never appeared until an unrelated transition happened to touch
it (#2244).
ChangeNamespaceService now records a REMOVED entry for the old tuple
of every version the feed already knew about, and an ACTIVE entry for
the new tuple of every version still active, both at the same instant
the rename happens. wasReportedAsAvailable moves from ExtensionService
to RepositoryService so both call sites share it.
A new migration backfills the same two entries for versions that were
already renamed before this fix, stamping changed_at with the
migration's own run time rather than a reconstructed rename time,
since none is known and a backdated entry would sort into a part of
the feed a caught-up consumer has already read past.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: withdraw every abandoned namespace tuple, not just the latest
A version could pick up a further, correctly-recorded transition
under its new namespace after a silent rename (e.g. an admin
deactivation, which always reads the version's current namespace),
without that ever withdrawing the stale tuple the rename abandoned.
Collapsing to one row per version, as before, hid that stale tuple:
its single latest entry already matched the current namespace, so the
old (still not-REMOVED) namespace was never even considered.
Track history per (version, namespace) for the departure side instead,
so an abandoned namespace's own latest state is checked independently
of what happened later under a different one. The arrival side keeps
using the version's single overall-latest entry, which is still the
right question for "has the feed caught up to where this version
lives now".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: scope wasReportedAsAvailable to the version's current namespace
main added V1_76__Unique_Active_Review.sql after this branch was cut,
colliding with this PR's migration version - renamed the migration to
V1_77.
More importantly: once a version's changes-feed history can span more
than one namespace (a rename backfill, or two renames over its life),
the single most recent entry for a version is no longer necessarily
the one that reflects its current namespace - an entry appended later
for an abandoned namespace (e.g. this PR's own backfill, timestamped
with the migration's run time) can outrank an earlier, correctly
recorded entry under the namespace the version actually lives in now.
wasReportedAsAvailable, read that way, would then answer for the wrong
tuple and skip or duplicate a withdrawal on a later delete or rename.
Added ExtensionVersionChangeRepository#findFirstByExtensionVersionAndNamespaceOrderByChangedAtDescIdDesc
and scoped wasReportedAsAvailable to it, resolving the latest state
for the version's current namespace specifically. Covered by a new
ExtensionVersionChangeRepositoryTest exercising the exact scenario.
Documented two remaining, narrower gaps as known limitations rather
than folding them into this fix: a concurrent writer (scan completion,
admin action) racing a rename isn't locked against today, and neither
is any other transition writer - pre-existing, not introduced here,
and fixing it means auditing every writer, not just this one. A
version renamed back to a namespace it occupied before is only
repaired correctly if that namespace's own last entry already agrees
with the version's current state - not a concern for any of the four
renames #2244 was filed about.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: set target_platform in ExtensionVersionChangeRepositoryTest
extension_version.target_platform has been NOT NULL since V1_22, so
persisting an ExtensionVersion without it failed CI's real-Postgres
run even though it compiled fine and the mocked unit tests never
touch the column.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: lock the affected tables before the migration's snapshots
A rolling deployment can have an old-version instance still serving
traffic while this migration runs on a new one. Without a lock, a
concurrent purge landing between the departures snapshot and its
insert would delete the extension_version row that snapshot already
captured the id of, failing the insert on the foreign key; a
concurrent rename landing between the departures and arrivals
snapshots would let one see the version under its old namespace and
the other under a namespace newer still, repairing neither correctly.
LOCK TABLE ... IN SHARE MODE on extension, extension_version and
extension_version_change before the first snapshot, same pattern
V1_76__Unique_Active_Review.sql already uses for the same class of
rolling-deployment race.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: verify namespaces at call time in ChangeNamespaceServiceTest
recordExtensionVersionChange takes no namespace argument - the real
implementation reads extVersion.getExtension().getNamespace() at call
time, from the same mutable Extension object changeExtensionNamespace
later mutates in place. The order-only assertion couldn't tell that
apart from a namespace already changed before either call: it would
still see REMOVED-then-ACTIVE, both against the new namespace.
Capture the namespace via doAnswer at each invocation instead, and
assert it was "old" for the departure and "new" for the arrival.
Verified this catches the regression by temporarily swapping
changeExtensionNamespace before recordNamespaceDeparture locally: the
new assertion fails while the order-only one still passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* style: compact comments added across this fix
Kept the why, cut the restating and repetition - same content in
roughly half the lines across the service, repository, migration and
tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: also fix extension_version_change_seq's missing OWNED BY here
Folded in from #2258 rather than shipping as its own V1_77, which
would collide with this PR's own V1_77 the moment both land - same
table, same migration file is also just a better fit: this PR is the
one already touching extension_version_change's schema semantics.
extension_version_change_seq was created (V1_71) without OWNED BY,
unlike every other entity's sequence (V1_40 added this same link for
the sequences that predated it). Without it, pg_get_serial_sequence
can't find the sequence, which silently defeats
scripts/import-db-dump.sh's generic per-table sequence reset for this
one table - discovered while testing #2258's db-dump tooling changes
against a production copy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* style: compact the migration's comments further
Kept the why, cut the restating - same content in fewer lines.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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
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.
Summary
scrub-db-dump.shandimport-db-dump.shalready agree on a per-table Postgres COPY-TEXT dump format, but nothing in the repo actually produces that dump from a live source - someone has to hand-roll the\copy ... tocommands themselves. Ran into this while testing #2246 against a production DB copy.scripts/export-db-dump.sh(new): exports the 9 tablesimport-db-dump.shexpects from a given Postgres connection string, all in one transaction/snapshot (REPEATABLE READ, READ ONLY) so concurrent writes on the source can't leave the tables inconsistent with each other. Stages into a temp directory and promotes each table's file individually oncepsqlhas committed - deliberately per-file rather than a directory-level swap, sincescrub-db-dump.shleaves*.csv.bakoriginals alongside the redacted files and a whole-directory swap would silently delete those (and anything else already there) on a routine re-export. This isn't atomic as a set: a failure partway through the promotion loop can leave a mixed generation, accepted as the better tradeoff over destroying files the export doesn't own (a stale CSV is fixed by re-running the export). Also checks forpsqlup front, restricts permissions on the raw (pre-redaction) dump, resolves symlinked output directories to their real path, and runspsql -Xso a~/.psqlrccan't override its error handling.scripts/import-db-dump.sh: addsextension_version_change(introduced by V1_71) to the table list. It predates that table, so a DB copy built via this pipeline had an always-emptyextension_version_changeregardless of production reality - nothing that reads the registry changes feed (/api/-/version-changes, and anything fix: report a namespace rename on the registry changes feed #2246 touches) could be exercised against such a copy. Its sequence reset also falls back to the<table>_seqnaming convention whenpg_get_serial_sequencecan't resolve a sequence (true forextension_version_changeon any schema before fix: report a namespace rename on the registry changes feed #2246's ownership fix, including a fresh target pinned to an older dump version).Depends on #2246 for full correctness:
extension_version_change_seq(V1_71) was created withoutOWNED BY, unlike every other entity's sequence, which meanspg_get_serial_sequencecan't find it on an unpatched schema. #2246'sV1_77now fixes that (folded in there instead of shipping a second, collidingV1_77here). The naming-convention fallback above means this still works correctly regardless of merge order, but the underlying schema gap is real either way.Test plan
bash -non both scripts.psqlstub: a pre-existing.csv.bakand an unrelated file both survive an export run untouched, all 9 real tables refresh.detect_actual_cols/version-detection empty-table handling verified against a real empty CSV and aset -earray-assignment repro.import-db-dump.shneeds for its scratch Postgres/Flyway containers) - worth a real run throughexport-db-dump.sh→scrub-db-dump.sh→import-db-dump.shbefore merging.🤖 Generated with Claude Code