Skip to content

fix(schema-ir): match int8 decimal-text defaults against their safe-integer number - #30194

Merged
SevInf merged 1 commit into
mainfrom
issue-30174
Sep 2, 2026
Merged

fix(schema-ir): match int8 decimal-text defaults against their safe-integer number#30194
SevInf merged 1 commit into
mainfrom
issue-30174

Conversation

@StevenMcClankerton

Copy link
Copy Markdown
Contributor

Summary

A literal @default on an int8 column made db migrate fail its own post-apply verification with MIGRATION.SCHEMA_VERIFY_FAILED. An int4 column with the same default verified fine.

Fixes #30174

The true root cause

parsePostgresDefault's isBigInt branch (packages/3-targets/3-targets/postgres/src/core/default-normalizer.ts:225-228) returns decimal text for every int8 default, regardless of DDL spelling. So even a bare DEFAULT 0 introspects back as "0" (string) against the contract's 0 (number) for pg/int8number@1. It is a string/number type asymmetry, not a syntax one.

Postgres does not re-derive a quoted cast for small bigint defaults: bigint DEFAULT 0 comes back as 0 — bare within int4 range, quoted+cast only beyond it. Verified on PostgreSQL 17.11 and PGlite 18.3, byte-identical:

bigint DEFAULT 0                              -> 0
bigint DEFAULT 9223372036854775807            -> '9223372036854775807'::bigint

The '0'::int8 our own DDL renders is ours, not Postgres's: pgRenderDdlColumnDefault runs the value through codec.encode, pgInt8NumberEncode returns a string, and pgInlineLiteral quotes and casts any string wire.

Why the fix cannot live on the render path or in postgresResolveDefault

postgresResolveDefault's output becomes resolvedDefault (packages/2-sql/9-family/src/core/migrations/contract-to-schema-ir.ts:127-129), which columnLike() (packages/3-targets/3-targets/postgres/src/core/migrations/column-ddl-rendering.ts:56) maps into the StorageColumn.default slot that reaches codec.decodeJson (packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts:1770). "One differ drives both verify and plan" (packages/3-targets/3-targets/postgres/src/core/migrations/diff-database-schema.ts:214).

The two codecs bound to the same resolvedNativeType are mutually incompatible: PgInt8NumberCodec.decodeJson requires a JSON number (codecs.ts:723-725 — its own doc comment calls this "the deliberate exception to the decimal-text rule ... and the codec's purpose"), while PgInt8Codec.decodeJson requires a decimal string (codecs.ts:660-668). Introspection has no codec identity to consult, so it cannot pick a shape per column, and any normalization performed on the shared resolvedDefault before it reaches DDL rendering corrupts one codec or the other. Confirmed with three live-database probes:

PROBE A  int8number + decimal-TEXT resolvedDefault (proposed normalize-to-string fix) -> pg/int8number@1 must be a number
PROBE B  int8@1     + NUMBER       resolvedDefault (the inverse: normalize-to-number)  -> pg/int8@1 must be a decimal string
PROBE C  int8number + NUMBER       resolvedDefault (this PR's fix: unchanged)          -> ok, stage execute, no failure

What the fix is

One spelling-gated branch in the module-private normalizeLiteralValue (packages/2-sql/1-core/schema-ir/src/ir/resolved-default-equality.ts), beside the existing temporal-normalization branch: a safe-integer number is compared against the decimal text it denotes, only under an int8/bigint native type. It normalizes rather than widens: literalValuesEqual is byte-for-byte unchanged, and resolvedDefaultsEqual's signature is unchanged. The normalized value lives for the one comparison call and never reaches resolvedDefault or the DDL renderer, so it cannot corrupt the codec-typed value pgRenderDdlColumnDefault depends on.

Soundness

String(n) is injective over safe integers, so no two distinct values collide. pgInt8NumberGuard (packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts:158-169) already rejects everything outside Number.isSafeInteger at both encode and decode, so the gate this fix adds is exactly coextensive with the codec's own domain and can produce no reachable false negative.

On the reporter's claim

He is right that BigInt and BigIntNumber produce byte-identical DDL. What's wrong is only the inference that the codec family causes it: pg/int8@1 carries "value": "0" and round-trips text→text; pg/int8number@1 carries "value": 0 and is the sole reproducer.

The layering residue, stated honestly

int8 is a Postgres alias for the standard SQL bigint, and this shared SQL-core package now names it. isTemporalNativeType directly above already matches timestamptz the same way, and bigint itself is standard SQL accepted by MySQL/MSSQL/SQLite. The clean fix is a comparison-only target hook — see follow-up below.

Testing

  • schema-ir: 264 passed
  • family-sql (packages/2-sql/9-family): 340 passed
  • adapter-postgres: 867 passed, 3 expected-fail, 0 failed
  • End-to-end on real PostgreSQL 17.11: applied and verified the reporter's model plus a Number.MAX_SAFE_INTEGER-adjacent int8 default with no precision loss
  • Unit coverage: the safe-integer match in both operand orders, a negative integer, a genuine mismatch, no effect without an int8/bigint native type, a rounded-number-vs-exact-text rejection, an outside-safe-integer-range rejection (even when the text is exact), and two huge decimal-text strings compared by identity

Follow-ups (not opened as issues; not fixed in this PR)

  • Comparison-only normalization seam. DefaultResolver (packages/2-sql/9-family/src/core/migrations/contract-to-schema-ir.ts:62-71) cannot carry this normalization because its output is dual-purpose (verify and plan/DDL). The proper home is a new comparison-only target hook alongside DefaultResolver / NativeTypeExpander / DefaultRenderer, so this equality file needs no int8/bigint spelling at all. That's an architecture change (Ask First) beyond this bug fix.
  • int8-family list-default crash. BigInt[] @default([1,2]) -> CLI.UNEXPECTED: pg/int8@1 database JSON value must be a decimal string; BigIntNumber[] @default([1,2]) -> ... must be a number. Cause: pgRenderDdlColumnDefault (control-adapter.ts:1770) hands the whole array to codec.decodeJson instead of decoding per element. Pre-existing, out of scope, and the same codec-shape constraint this fix navigates — the reporter's money model has 22 defaulted int8 columns.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UcqoY3CKfnubdZt5YQk2Rq

…nteger number

A literal @default on an int8 column made db migrate fail its own
post-apply verification: Postgres's own introspection normalizer
(parsePostgresDefault's isBigInt branch) always reads an int8/bigint
default back as decimal text, regardless of DDL spelling, so
pg/int8number@1's JSON-number canonical default (e.g. 0) could never
match the introspected "0" it produced.

The fix lives in resolvedDefaultsEqual's comparison-only helper,
normalizeLiteralValue, beside the existing temporal-normalization
branch: a safe-integer number is compared against the decimal text it
denotes only under an int8/bigint native type. literalValuesEqual
stays byte-for-byte unchanged and the normalized value never reaches
resolvedDefault or DDL rendering, so it cannot corrupt the codec-typed
value pgRenderDdlColumnDefault depends on for pg/int8@1 (decimal
string) or pg/int8number@1 (JSON number).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UcqoY3CKfnubdZt5YQk2Rq
Signed-off-by: Steven McClankerton <tatarintsev@prisma.io>
@StevenMcClankerton
StevenMcClankerton requested a review from a team as a code owner September 1, 2026 17:31
@pkg-pr-new

pkg-pr-new Bot commented Sep 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma/orm-extension-arktype-json

npm i https://pkg.pr.new/@prisma/orm-extension-arktype-json@30194

@prisma/orm-extension-middleware-cache

npm i https://pkg.pr.new/@prisma/orm-extension-middleware-cache@30194

@prisma/orm-extension-paradedb

npm i https://pkg.pr.new/@prisma/orm-extension-paradedb@30194

@prisma/orm-extension-pgvector

npm i https://pkg.pr.new/@prisma/orm-extension-pgvector@30194

@prisma/orm-extension-postgis

npm i https://pkg.pr.new/@prisma/orm-extension-postgis@30194

@prisma/orm-extension-supabase

npm i https://pkg.pr.new/@prisma/orm-extension-supabase@30194

@prisma/orm-family-mongo

npm i https://pkg.pr.new/@prisma/orm-family-mongo@30194

@prisma/orm-family-sql

npm i https://pkg.pr.new/@prisma/orm-family-sql@30194

@prisma/orm-framework

npm i https://pkg.pr.new/@prisma/orm-framework@30194

@prisma/orm-mongo

npm i https://pkg.pr.new/@prisma/orm-mongo@30194

@prisma/orm-postgres

npm i https://pkg.pr.new/@prisma/orm-postgres@30194

@prisma/orm-sqlite

npm i https://pkg.pr.new/@prisma/orm-sqlite@30194

@prisma/orm-target-mongo

npm i https://pkg.pr.new/@prisma/orm-target-mongo@30194

@prisma/orm-target-postgres

npm i https://pkg.pr.new/@prisma/orm-target-postgres@30194

@prisma/orm-target-sqlite

npm i https://pkg.pr.new/@prisma/orm-target-sqlite@30194

@prisma/orm-toolchain

npm i https://pkg.pr.new/@prisma/orm-toolchain@30194

commit: 236aba9

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
postgres / no-emit 174.92 KB (+0.03% 🔺)
postgres / emit 152.09 KB (+0.04% 🔺)
mongo / no-emit 101.09 KB (0%)
mongo / emit 90.95 KB (0%)
cf-worker / no-emit 198.84 KB (+0.04% 🔺)
cf-worker / emit 173.37 KB (+0.04% 🔺)

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

An error occurred during the review process. Please try again later.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-30174

Comment @coderabbitai help to get the list of available commands.

@SevInf
SevInf added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit b7a8bd2 Sep 2, 2026
26 checks passed
@SevInf
SevInf deleted the issue-30174 branch September 2, 2026 08:45
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.

int8 literal @default renders as DEFAULT '0'::int8, so db migrate fails its own schema verification

2 participants