Skip to content

Type-safe extractEncryptionSchemaV3: preserve per-column concrete types for Drizzle-extracted schemas #589

Description

@tobyhede

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)

  1. 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 }).
  2. 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.
  3. 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

  • extractEncryptionSchemaV3 returns a precisely-typed EncryptedTable<...> inferred from the Drizzle table's encrypted columns.
  • .test-d.ts coverage proving both paths stay correct: (a) the extracted-Drizzle insert path, and (b) the existing hand-declared encryptedTable({...}) path.
  • The 7 latent errors in operators-live-pg.test.ts are resolved with the casts removed.
  • No runtime behavior change; no regression to non-Drizzle v3 consumers.

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions