Summary
extractEncryptionSchemaV3 (EQL v3 Drizzle adapter) erases per-column concrete types, returning the widened AnyV3Table. As a result, inserts against an extracted schema are not fully type-safe: InferPlaintext over the widened schema collapses columns to an index-signature shape that doesn't match a concrete row, so plain (non-encrypted) helper columns don't type-match on insert.
This is a type-safety / DX gap only — there is no runtime bug. The runtime already recovers each column's concrete builder correctly, encrypt/insert/select/decrypt all work, and the select → decryptModel typing is already clean. It's the insert-side typing against an extracted schema that widens.
Impact
- 7 latent type errors remain in
packages/stack/__tests__/drizzle-v3/operators-live-pg.test.ts (down from 20 after removing as never/Record<string, unknown> casts). They are "latent": the file is a .test.ts, which is outside the vitest typecheck scope and is transpiled by esbuild without type-checking — so they don't fail CI or the build today.
- Users following the documented Drizzle flow (
extractEncryptionSchemaV3(table) → EncryptionV3({ schemas }) → bulkEncryptModels) get widened insert typing rather than precise per-column plaintext types.
Root cause
The v3 core is already built for precise typing:
EncryptedTable<T> is generic over the exact per-column map; encryptedTable<T>(name, columns: T) preserves T.
InferPlaintext<T> / InferEncrypted<T> map each concrete column to its precise plaintext/envelope type.
But extractEncryptionSchemaV3(table: PgTable): AnyV3Table builds columns: Record<string, AnyEncryptedV3Column> (widened) and returns AnyV3Table, discarding T. makeEqlV3Column stashes the concrete builder at runtime (symbol carrier) but its returned Drizzle column type has forgotten which builder it wraps, so there is nothing at the type level for extraction to recover.
Proposed fix (three linked pieces)
makeEqlV3Column carries the concrete builder at the type level (src/eql/v3/drizzle/column.ts) — add a recoverable phantom brand, e.g. the returned column type parameterised by its builder C (EqlV3Column<C> = <drizzle column> & { readonly __v3Builder?: C }).
extractEncryptionSchemaV3 becomes generic (src/eql/v3/drizzle/schema-extraction.ts) — <T extends PgTable>(table: T): EncryptedTable<BuildersOf<T>>, where BuildersOf<T> is a mapped type that recovers each column's brand and keeps only the branded (encrypted) ones — the type-level mirror of the existing runtime loop. Runtime is unchanged; only the return type becomes precise.
- Adjust core inference types as needed (
src/eql/v3/table.ts, src/eql/v3/columns.ts) — make the brand flow cleanly through EncryptedTable / EncryptedV3TableColumn / PlaintextForColumn / InferPlaintext.
Risk / why it needs its own PR
- Advanced type-level TS: a mapped/conditional type over Drizzle's deeply-generic
customType columns that recovers a phantom brand and filters non-encrypted columns, without breaking Drizzle's native insert/select inference.
- Pieces 3 touch v3 core typing, which is load-bearing for every v3 consumer (the whole typed-client surface), not just Drizzle. A regression there affects all v3 users.
- Should land as a standalone, independently-reviewed PR — not bundled into adapter feature work.
Acceptance criteria
Interim (optional, separate)
Narrow the 7 test-file errors locally (typed helper / minimal casts) without touching core — closes the latent errors but does not give users type-clean extracted inserts.
References
packages/stack/src/eql/v3/drizzle/schema-extraction.ts
packages/stack/src/eql/v3/drizzle/column.ts (makeEqlV3Column)
packages/stack/src/eql/v3/table.ts (EncryptedTable, InferPlaintext, InferEncrypted)
packages/stack/src/eql/v3/columns.ts (PlaintextForColumn)
packages/stack/__tests__/drizzle-v3/operators-live-pg.test.ts (latent errors)
Surfaced during the EQL v3 Drizzle adapter type-safety pass (A3/M1).
Summary
extractEncryptionSchemaV3(EQL v3 Drizzle adapter) erases per-column concrete types, returning the widenedAnyV3Table. As a result, inserts against an extracted schema are not fully type-safe:InferPlaintextover the widened schema collapses columns to an index-signature shape that doesn't match a concrete row, so plain (non-encrypted) helper columns don't type-match on insert.This is a type-safety / DX gap only — there is no runtime bug. The runtime already recovers each column's concrete builder correctly, encrypt/insert/select/decrypt all work, and the
select → decryptModeltyping is already clean. It's theinsert-side typing against an extracted schema that widens.Impact
packages/stack/__tests__/drizzle-v3/operators-live-pg.test.ts(down from 20 after removingas never/Record<string, unknown>casts). They are "latent": the file is a.test.ts, which is outside the vitest typecheck scope and is transpiled by esbuild without type-checking — so they don't fail CI or the build today.extractEncryptionSchemaV3(table)→EncryptionV3({ schemas })→bulkEncryptModels) get widened insert typing rather than precise per-column plaintext types.Root cause
The v3 core is already built for precise typing:
EncryptedTable<T>is generic over the exact per-column map;encryptedTable<T>(name, columns: T)preservesT.InferPlaintext<T>/InferEncrypted<T>map each concrete column to its precise plaintext/envelope type.But
extractEncryptionSchemaV3(table: PgTable): AnyV3Tablebuildscolumns: Record<string, AnyEncryptedV3Column>(widened) and returnsAnyV3Table, discardingT.makeEqlV3Columnstashes the concrete builder at runtime (symbol carrier) but its returned Drizzle column type has forgotten which builder it wraps, so there is nothing at the type level for extraction to recover.Proposed fix (three linked pieces)
makeEqlV3Columncarries the concrete builder at the type level (src/eql/v3/drizzle/column.ts) — add a recoverable phantom brand, e.g. the returned column type parameterised by its builderC(EqlV3Column<C> = <drizzle column> & { readonly __v3Builder?: C }).extractEncryptionSchemaV3becomes generic (src/eql/v3/drizzle/schema-extraction.ts) —<T extends PgTable>(table: T): EncryptedTable<BuildersOf<T>>, whereBuildersOf<T>is a mapped type that recovers each column's brand and keeps only the branded (encrypted) ones — the type-level mirror of the existing runtime loop. Runtime is unchanged; only the return type becomes precise.src/eql/v3/table.ts,src/eql/v3/columns.ts) — make the brand flow cleanly throughEncryptedTable/EncryptedV3TableColumn/PlaintextForColumn/InferPlaintext.Risk / why it needs its own PR
customTypecolumns that recovers a phantom brand and filters non-encrypted columns, without breaking Drizzle's native insert/select inference.Acceptance criteria
extractEncryptionSchemaV3returns a precisely-typedEncryptedTable<...>inferred from the Drizzle table's encrypted columns..test-d.tscoverage proving both paths stay correct: (a) the extracted-Drizzle insert path, and (b) the existing hand-declaredencryptedTable({...})path.operators-live-pg.test.tsare resolved with the casts removed.Interim (optional, separate)
Narrow the 7 test-file errors locally (typed helper / minimal casts) without touching core — closes the latent errors but does not give users type-clean extracted inserts.
References
packages/stack/src/eql/v3/drizzle/schema-extraction.tspackages/stack/src/eql/v3/drizzle/column.ts(makeEqlV3Column)packages/stack/src/eql/v3/table.ts(EncryptedTable,InferPlaintext,InferEncrypted)packages/stack/src/eql/v3/columns.ts(PlaintextForColumn)packages/stack/__tests__/drizzle-v3/operators-live-pg.test.ts(latent errors)Surfaced during the EQL v3 Drizzle adapter type-safety pass (A3/M1).