fix(pgsql): key routines by signature so overloads survive the diff (#187) - #188
Merged
Merged
Conversation
…187) getRoutines() keyed its map on proname, but Postgres allows several functions to share a name with different argument types. Overloads overwrote each other, so N-1 were invisible to the diff entirely — genuine drift in them was missed — and which one survived depended on row order, which differs between databases. Reproduced on PostgreSQL 16 with three identical cosine_distance overloads created in a different order in each database. The schemas hash identically (058b5b2e16392bd9710a81b99769fa24 both sides), but getRoutines' own query returns them in different orders, so one side kept the bigint overload and the other the integer one. dbdiff then generated a migration for two identical schemas — and the migration did not even run: DROP FUNCTION IF EXISTS "cosine_distance"; ERROR: function name "cosine_distance" is not unique HINT: Specify the argument list to select the function unambiguously. Had it been unambiguous it would have removed all three overloads to recreate one. Routines are now keyed by p.oid::regprocedure::text — "cosine_distance (integer,integer)" — which is unique per overload and stable across databases, unlike oid. Ordering by the same expression makes the result deterministic where ORDER BY proname left ties unordered. The DROP has to carry the argument list to match. RoutineDrop::build() appends it outside the quoted identifier, since quoting the whole signature yields "fn(integer,text)" — one odd identifier rather than a call signature. A bare name (MySQL, which has no overloading) passes through unchanged. Three SQL generators carried identical private copies of the drop builder, so this consolidates them rather than writing the argument handling three times — CreateRoutineSQL was the third and was initially missed, which the golden-file tests caught. Triggers had the same class of bug: tgname is unique per table, not per schema, so two tables sharing a trigger name silently lost one. Now keyed by table and name, with the bare name carried alongside so the emitted DDL is unchanged. Verified end to end: identical schemas now report no drift, and a genuinely changed overload emits DROP FUNCTION IF EXISTS "cosine_distance"(text,text) which applies cleanly and leaves the sibling overloads intact. Golden fixtures for pgsql 14-18 updated for the argument-qualified DROP — the bare form was the bug. 803 tests pass, 15 of them new.
dolthub/dolt-sql-server:latest was the only unpinned database image in this
repo — mysql and postgres are all version-pinned, in CI and in
docker-compose. On 2026-08-13 latest moved from 2.2.3 to 2.3.0 and the Dolt
matrix started failing on branches that had not touched the code.
The failures are Dolt-side and non-deterministic: two runs of the same
commit errored 9 tests and then 4, on different tests each time, all with
PDOException: SQLSTATE[HY000]: General error: 1105 context canceled
thrown from loadFixture() while executing the fixture .sql — plain DDL,
before any DBDiff code runs. The sidecar log shows the server cancelling
in-flight queries alongside "stats stopped: context canceled". 2.2.3 ran the
same suite with zero such errors.
Pinned to 2.2.3 in CI, and docker-compose now honours the DOLT_VERSION
variable that .env.example already defined but nothing read, so local runs
match CI. Worth revisiting once a Dolt release fixes the cancellation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4LrS3symKm53ya9Cf7xun
|
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.



Fixes #187.
getRoutines()keyed its map onproname, but Postgres allows several functions to share a name with different argument signatures. Overloads overwrote each other, so N−1 were invisible to the diff entirely — genuine drift in them was missed — and which one survived depended on row order, which differs between databases.Reproduced
Three identical
cosine_distanceoverloads, created in a different order in each database. The schemas are content-identical (both hash to058b5b2e16392bd9710a81b99769fa24), butgetRoutines' own query returns them in different orders:Last row wins, so one side kept
bigintand the otherinteger, and dbdiff generated a migration for two identical schemas. That migration did not even run:Had it been unambiguous it would have removed all three overloads to recreate one.
Fix
Key by signature.
p.oid::regprocedure::textrenders ascosine_distance(integer,integer)— unique per overload and stable across databases, unlikeoid. Ordering by the same expression makes the result deterministic, whereORDER BY pronameleft ties unordered.Carry the argument list into the DROP.
RoutineDrop::build()appends it outside the quoted identifier — quoting the whole signature yields"fn(integer,text)", one odd identifier rather than a call signature. A bare name (MySQL, which has no overloading) passes through unchanged.Three SQL generators carried identical private copies of the drop builder, so this consolidates them rather than writing the argument handling three times.
CreateRoutineSQLwas the third and I initially missed it — the golden-file tests caught it, emitting"get_product_count()"with the parens inside the quotes.Triggers had the same class of bug.
tgnameis unique per table, not per schema, so two tables sharing a trigger name silently lost one — confirmed on a live server. Now keyed by table and name, with the bare name carried alongside so the emitted DDL is unchanged.Verification
End to end against PostgreSQL 16:
DROP FUNCTION IF EXISTS "cosine_distance"(text,text);, which applies cleanly and leaves the other two overloads intact (2 remaining, not 0).803 tests pass, 15 of them new: 9 unit tests for the drop builder (argument list placement, bare names, PROCEDURE vs FUNCTION detection, both Alter directions) and 6 live-server tests for overload retention, signature keys, per-overload definitions, deterministic ordering, and the trigger collision.
Golden fixtures
programmable_objects_pgsql_14through18updated for the argument-qualified DROP. The bare form was the bug, so the fixtures were encoding it. Both affected functions are zero-argument, so the change is"name";→"name"();identically on every version — verified directly on 16, and CI covers 14/15/17/18.PostgresAdapterstays at 20 methods; the newRoutineDrophas 2.Second commit: pinning the Dolt image
Unrelated to the fix, but it is what was holding CI red, so it rides along.
dolthub/dolt-sql-server:latestwas the only unpinned database image in the repo — mysql and postgres are version-pinned everywhere, in CI and in docker-compose. On 2026-08-13latestmoved 2.2.3 → 2.3.0, after master's last green run, and the Dolt matrix started failing on branches that had not touched the code.The failures are Dolt-side and non-deterministic — two runs of the same commit errored 9 tests and then 4, on different tests each time, all with:
thrown from
loadFixture()while executing the fixture.sql. That is plain DDL, before any DBDiff code runs, and the sidecar log shows the server cancelling in-flight queries alongsidestats stopped: context canceled. The last master run on 2.2.3 had zero such errors.Pinned to 2.2.3 in CI, and
docker-compose.ymlnow reads theDOLT_VERSIONvariable that.env.examplealready defined but nothing consumed, so local runs match CI. Worth revisiting once a Dolt release fixes the cancellation.🤖 Generated with Claude Code