Reject unknown values in the table extra config array - #6141
Open
Ash20pk wants to merge 1 commit into
Open
Conversation
The extra config callback is typed to return builders, but `stripInternal`
removes every `@internal` member from the published `.d.ts`, and
`PrimaryKeyBuilder` had nothing else on it. An empty object type accepts any
object, so the union member made the whole array permissive: wrapping a builder
in an object, or returning something that isn't builder-shaped at all,
typechecked cleanly. `getTableConfig` then walked the array matching builders by
entity kind and skipped everything it didn't recognise, so the declared index or
constraint never reached the database and no drift check could surface it -- the
schema, the migrations and the database all agreed it wasn't there.
Give the affected builders a declared brand so they stay nominal after
declaration emit, and throw from `getTableConfig` on anything that isn't a
builder. `generate` and `push` go through `getTableConfig`, so they now fail
loudly instead of dropping the entry.
`MySqlUniqueConstraintBuilder` needed the same treatment, and widening it
exposed that `MySqlTableExtraConfigValue` referenced the builder without a type
argument, defaulting `TName` to `undefined`. `unique('name').on(...)` had been
matching the empty `PrimaryKeyBuilder` instead; the union now takes
`string | undefined`.
The nominality test asserts against the real declaration emit rather than the
source, since the source keeps its internal members and would not catch a
regression.
Fixes drizzle-team#6140
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 #6140. Targeting
betasince the report is against1.0.0-rc.4.The problem
Two things compounding each other.
The published types accept anything. The build strips
@internalmembers from the emitted.d.ts(stripInternal: true).PrimaryKeyBuilderhad every instance member marked internal —columns,name,build()— and constructors and statics don't contribute to an instance type. So the published type is structurally empty, and an empty object type accepts any object. Because it's one member of the extra-config union, the whole array became permissive:getTableConfigdropped what it didn't recognise. Theif / else ifchain overis(builder, …)had no finalelse, so anything unrecognised was skipped in silence.generateandpushboth go throughgetTableConfig, which is why a declared index simply never appeared in a migration.The combination is what makes it nasty: the schema file, the migrations and the database all agree the index isn't there, so no drift check can surface it. And
[{ key: builder }]is exactly what you get by mechanically wrapping the old deprecated object form in an array.Scope
PrimaryKeyBuilderwas structurally empty in pg, cockroach, mysql, mssql and singlestore.sqlite-corewas already correct — it carriesdeclare _: { brand: 'SQLitePrimaryKeyBuilder' }, which is the pattern this PR applies everywhere else.MySqlUniqueConstraintBuilderhad the same problem (itsnameis@internalrather thanprivate, unlike the other dialects').The issue also names
AnyIndexBuilder, but that one is fine — it's an interface with abuild()method and rejects plain objects.The fix
Nominal builders.
declare _: { brand: '…' }on the fivePrimaryKeyBuilders and onMySqlUniqueConstraintBuilder, matching sqlite's existing precedent. Declaration-only, so no runtime emit and no behaviour change.Throw instead of skip. A final
elsein all sixgetTableConfigimplementations, via a sharedthrowUnknownExtraConfigValuehelper intable.utils.ts. It names the table and, for objects, points at the likely mistake:A latent mysql bug this uncovered. Branding
MySqlUniqueConstraintBuilderbroketype-tests/mysql/tables.ts, which revealed thatMySqlTableExtraConfigValuereferenced the builder without a type argument — defaultingTNametoundefined, sounique('some_name').on(…)never actually matched it. It had been matching the emptyPrimaryKeyBuilderinstead. The union now saysUniqueConstraintBuilder<string | undefined>.Tests
drizzle-orm/tests/table-extra-config.test.ts, 30 tests in two halves.Runtime, parameterised over all six dialects: the array form still builds the index, the wrapped-in-an-object form throws, a non-builder value throws.
Declaration emit. This half needed care. Type tests here compile against
src, where the internal members are still visible — so a@ts-expect-errortest would have passed even with the bug present. Instead it runsts.transpileDeclarationwithstripInternalover the real sources and asserts each builder still has at least one instance member in the emitted declaration. That checks the artifact users actually consume, which is where the bug lived.Reverting only
src/fails 18 of the 30, so it's a genuine regression test.Verification
drizzle-ormsuite: 994 pass, 3 fail —sql-builderand twocasingfiles, which fail identically on unmodifiedbeta. Pre-existing and unrelated.tsc -p type-tests/tsconfig.jsonexits 0.oxlint --max-warnings=0anddprintclean.dist/:goodcompiles,badandworseare now compile errors, andgetTableConfig(bad)throws the message above.One thing worth a maintainer's opinion
Throwing means an
undefinedorfalseelement in the array — from a conditional, say — now errors where it used to be skipped. TypeScript already rejected that shape, so it's consistent, but it is the sharp edge of failing loudly. Happy to downgrade to a warning if you'd rather not have that in a 1.0.I left
changelogs/drizzle-orm/alone since those look release-scoped; glad to add an entry wherever you want it.