fix: align child column type on the referenced key in PostgreSQL output - #36
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## main #36 +/- ##
==========================================
+ Coverage 92.09% 92.24% +0.15%
==========================================
Files 79 79
Lines 3161 3225 +64
==========================================
+ Hits 2911 2975 +64
Misses 250 250
🚀 New features to boost your workflow:
|
mcoutantL
approved these changes
Sep 21, 2026
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.
Summary
PostgresSqlEmittermapped every column in isolation, so the two sides of aforeign key could land on PostgreSQL types the server cannot compare. The DDL
and the data were valid, but the final
ALTER TABLE ... ADD FOREIGN KEYfailed:
Since a dump is wrapped in a single transaction, that one error rolled the
whole import back and the user got no data at all. Source schemas declare the
two sides with different types more often than one would expect —
uniqueidentifier/varchar(36),int/numeric(18,0)andbit/tinyintwere all seen in the field.A child column carrying a foreign key now takes the type of the key it
references, but only when PostgreSQL refuses the pair. Every other column keeps
its declared type, so the output is unchanged for schemas that were already
importable.
The compatibility rule is encoded in three module tables, probed pair by pair
on a live PostgreSQL 15 server. Three of its properties are not obvious:
varchar(50)may referencevarchar(10);integercolumn may reference anumerickey, but a
numericcolumn may not reference anintegerkey;char/varchar/textaccept each other, and so dodate/timestamp/
timestamptz.The defect was reported against 1.0.1 with a patch against that version. The
patch could not be applied as it stood: it read
column.foreign_key, which nolonger exists since foreign keys became table-level, possibly composite
constraints. The resolution step was rewritten around
table.foreign_keys,zipping
columnsandref_columnsto find what a given local column pointsat. Composite keys are therefore handled — each half is aligned on its own
counterpart, which matches the pairwise check PostgreSQL performs.
Both the dump mode and the migrate mode go through
emit_tables, so both getthe fix.
Type of change
fix:) — non-breaking change that fixes an issuefeat:) — non-breaking change that adds capabilityperf:)refactor:) — no behaviour changedocs:)ci:/chore:)column_definitiongains a second parameter with a default value, so thesignature stays source-compatible;
emit_tablesis its only caller that passesit.
Linked issues
How was this tested?
Fourteen new test cases (29 parametrized runs) in
tests/unit/infrastructure/emit/test_postgres_emitter.py, covering:character varying,int4,bpchar,timestamp without time zone), which must not trigger arewrite;
integer/bigintrather thanserial/bigserial, and an identity child, which is left alone;filter may leave behind;
The reproduction from the report was replayed end to end on the SQLite driver:
dump.sqlnow declares"client_id" uuidinstead of"client_id" varchar(36).pytest tests/unit tests/clipasses locally — 627 passedlint-importspasses (no driver leaked into domain/application) — 2 contracts kepttox -e syntaxequivalent passes — black, isort, flake8, mypy clean, pylint 10.00/10tests/functionalran against the docker stackThe functional suite was not run: no Docker and no PostgreSQL server were
available in the environment used to prepare this change. The live import check
therefore rests on the compatibility table supplied with the report, probed on
PostgreSQL 15.19, rather than on an execution here. Running
tests/functionalagainst the stack before merge is advisable.Checklist
docs/andCHANGELOG.md— nopublic API change;
CHANGELOG.mdis generated bysemantic-releaseoverall, 99% on the changed file
Notes for the reviewer
Where the alignment is decided.
_aligned_typereturnsNonewheneverthe declared type is already acceptable, which is the case for every column
without a foreign key. The emitted output is byte-for-byte identical for
schemas that did not hit the defect.
Type aliases matter. A PostgreSQL source reports
character varying, andthe type map leaves that spelling alone because it is already valid
PostgreSQL. Without
_TYPE_ALIASESfolding it tovarchar, the emitter wouldsee two different types and rewrite columns that need no change. That is what
keeps the PG-to-PG path inert.
Identity keys. A key column reads
serialorbigserialin the DDL, buta column referencing it must be declared
integerorbigint._key_typereturns the storage type for that reason, and
_aligned_typerefuses to touchan identity child.
Cycles.
_key_typefollows a key that is itself a foreign key down to theroot. The
seenset stops a self-reference or a cycle between two tables;both are covered by tests.
A column named by two constraints keeps the first one. Aligning on both is
not possible in the general case, and the situation does not arise in any
schema we have seen.
Side effect worth knowing. The
DEFAULTclause is now translated againstthe aligned type, so a source
0on a column aligned tobooleanemitsFALSEinstead of an integer PostgreSQL would reject. A test locks this in.Deliberately left out. The report named two neighbouring defects that this
PR does not address:
UNIQUEconstraint, so a foreign key pointingat a unique non-primary-key column always fails with
there is no unique constraint matching given keys for referenced table;emit_foreign_keysvalidates the referenced schema and table but not thereferenced column, so a filter that removes a column leaves an
ALTER TABLEnaming a column that does not exist.Both are reproducible and deserve their own PRs.
The MSSQL emitter has the same shape of defect and was not touched here.