Skip to content

fix(stack-drizzle): emit unqualified eql_v3 domain so drizzle-kit DDL is valid (rc.2 M5)#688

Merged
coderdan merged 2 commits into
mainfrom
fix/drizzle-kit-eql-v3-ddl
Jul 18, 2026
Merged

fix(stack-drizzle): emit unqualified eql_v3 domain so drizzle-kit DDL is valid (rc.2 M5)#688
coderdan merged 2 commits into
mainfrom
fix/drizzle-kit-eql-v3-ddl

Conversation

@coderdan

@coderdan coderdan commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

drizzle-kit generate/push emitted invalid DDL for EQL v3 encrypted columns. A v3 column declared its SQL type as the schema-qualified domain public.eql_v3_text_search, but drizzle-kit wraps a customType's whole dataType() string in one pair of double quotes:

-- before (broken):
CREATE TABLE "users" (
  "email" "public.eql_v3_text_search"   -- one dotted identifier → type does not exist
);

-- after (fixed):
CREATE TABLE "users" (
  "email" "eql_v3_text_search"          -- resolves via search_path (domains live in public)
);

Postgres reads "public.eql_v3_text_search" as a single type name containing a dot and rejects it (type "public.eql_v3_text_search" does not exist), so every generated migration needed hand repair.

Fix

makeEqlV3Column now emits the bare domain name (eql_v3_text_search) via stripDomainSchema. This:

  • renders as the valid "eql_v3_text_search" in generated DDL, resolving through the search path (the eql_v3_* domains are created in public, always in-path);
  • mirrors the v2 encryptedType surface, which already emits its bare type for exactly this reason (see the comment block in packages/stack-drizzle/src/index.ts);
  • matches what drizzle-kit introspection reads back on a push diff, so the code side and DB side stop disagreeing (the reported "undefined"."public.eql_v3_*" mode).

Builder recovery keeps the canonical identity: getSqlType() re-qualifies a bare name back to public.eql_v3_* before matching the recovery keys, and still accepts an already-qualified name (test doubles / introspected snapshots) — so operators and schema extraction are unchanged.

Reproduction

Confirmed against drizzle-kit@0.30.6 via drizzle-kit/api generateMigration: the qualified name produced "public.eql_v3_text_search" on both CREATE TABLE and ADD COLUMN; the bare name produces the valid quoted form. (In 0.30.6 the ALTER path already emits an unquoted, valid name; the "undefined". double-break is an older-drizzle-kit ALTER artifact and shares this root cause.)

Tests

  • New regression guard: every V3_MATRIX domain's getSQLType() asserted dot-free.
  • New bare-getSQLType recovery test alongside the existing qualified one.
  • Updated the two tests that pinned the qualified getSQLType() to the bare form.
  • 370 unit green, test:types clean, build + biome clean.

Skill

skills/stash-drizzle/SKILL.md updated to describe the unqualified generated type and the search-path requirement (hence the stash patch changeset — the skill ships in the tarball).

Changeset

@cipherstash/stack-drizzle patch + stash patch.

https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w

Summary by CodeRabbit

  • Bug Fixes
    • Fixed generated migration SQL for EQL v3 encrypted columns by correcting invalid quoted type identifiers.
    • Improved compatibility with Drizzle Kit generate/push so v3 column domain types are emitted correctly.
    • Ensured existing encrypted column types are correctly recognized and recovered.
  • Documentation
    • Clarified EQL v3 generated type naming and the need for the PostgreSQL search path to include public.
  • Tests
    • Updated and expanded v3 column/type assertions to prevent regressions.
  • Chores
    • Published patch updates to related packages.

… is valid (rc.2 M5)

A v3 encrypted column declared its SQL type as the schema-qualified domain
(`public.eql_v3_text_search`). drizzle-kit wraps a customType's whole `dataType()`
string in one pair of double quotes, so `generate`/`push` emitted the invalid
identifier `"public.eql_v3_text_search"` — Postgres reads that as a single dotted
type name and rejects it (`type "public.eql_v3_text_search" does not exist`).
Reproduced against drizzle-kit 0.30.6: CREATE TABLE and ADD COLUMN both emit the
broken quoted form; generated migrations needed hand repair.

Fix: `makeEqlV3Column` now emits the BARE domain (`eql_v3_text_search`) via
`stripDomainSchema`, which drizzle-kit renders as the valid `"eql_v3_text_search"`
and which resolves through the search_path (the domains live in `public`). This
mirrors the v2 `encryptedType` surface, which already emits its bare type for the
same reason, and it matches what drizzle-kit introspection reads back on a `push`
diff so the two sides stop disagreeing (the `"undefined"."public.eql_v3_*"` mode).

Builder recovery is unchanged in identity: `getSqlType()` re-qualifies a bare name
back to the canonical `public.eql_v3_*` before matching the recovery keys, and it
still accepts an already-qualified name (test doubles / introspected snapshots),
so operators and schema extraction see the same domain as before.

- Regression guard: every V3_MATRIX domain's `getSQLType()` is asserted dot-free.
- Bare-getSQLType recovery test added alongside the existing qualified one.
- stash-drizzle skill updated to describe the unqualified generated type + the
  search-path requirement.

Tests: stack-drizzle 370 unit green, test:types clean, build + biome clean.
Changeset: stack-drizzle patch + stash patch (skill ships in the tarball).

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
@coderdan
coderdan requested a review from a team as a code owner July 17, 2026 11:29
@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4956900

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@cipherstash/stack-drizzle Patch
stash Patch
@cipherstash/bench Patch
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/stack Patch
@cipherstash/stack-supabase Patch
@cipherstash/prisma-next Patch
@cipherstash/wizard Patch
@cipherstash/test-kit Patch
@cipherstash/prisma-next-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5188723c-ce3f-454d-83f6-7bb108d5ce35

📥 Commits

Reviewing files that changed from the base of the PR and between 615378a and 4956900.

📒 Files selected for processing (2)
  • packages/stack-drizzle/__tests__/v3/column.test.ts
  • packages/stack-drizzle/src/v3/column.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/stack-drizzle/tests/v3/column.test.ts
  • packages/stack-drizzle/src/v3/column.ts

📝 Walkthrough

Walkthrough

EQL v3 Drizzle columns now emit unqualified domain names for generated DDL, while column recovery normalizes them to canonical public-qualified identities. Tests, release metadata, and skill documentation reflect the updated behavior.

Changes

EQL v3 DDL domain handling

Layer / File(s) Summary
Domain normalization and emitted types
packages/stack-drizzle/src/v3/column.ts
Bare EQL v3 domains are qualified during type recovery; custom types emit unqualified eql_v3_* names and enforce the public schema.
Behavior validation and documentation
packages/stack-drizzle/__tests__/v3/*.test.ts, .changeset/drizzle-kit-eql-v3-ddl.md, skills/stash-drizzle/SKILL.md
Tests cover unqualified SQL types, qualified builder recovery, and invalid schemas; release notes and skill documentation describe the updated DDL and search-path behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: freshtonic, calvinbrewer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: emitting unqualified EQL v3 domains to fix valid drizzle-kit DDL.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 fix/drizzle-kit-eql-v3-ddl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

Fixes invalid DDL from drizzle-kit generate/push for EQL v3 encrypted columns (rc.2 M5). A v3 column declared its SQL type as the schema-qualified domain public.eql_v3_text_search, but drizzle-kit wraps a customType's whole dataType() string in a single pair of double quotes — emitting "public.eql_v3_text_search", which Postgres reads as one dotted identifier and rejects. The fix emits the bare domain (eql_v3_text_search) via stripDomainSchema, which renders as valid "eql_v3_text_search" and resolves through search_path (the domains live in public). Builder recovery re-qualifies the bare name back to the canonical public.eql_v3_* identity, so operators and schema extraction are untouched.

Small, correct, well-reasoned. LGTM / approvable — details below; the only notes are nits.

Why this is right

  • The root-cause diagnosis is accurate and the fix is minimal. drizzle-kit's quote-the-whole-dataType() behaviour is real and is already documented on the v2 surface (packages/stack-drizzle/src/index.ts:15 — "unconditionally wraps the dataType() return in double-quotes"). This brings v3 in line with that established v2 convention.
  • It aligns the column with the rest of the v3 surface, which already stripped the schema. operators.ts:134 already calls stripDomainSchema(eqlType) (public.eql_v3_text_search → eql_v3_text_search), and stack-supabase/src/verify.ts compares against stripDomainSchema(builder.getEqlType()). So dataType() was the odd one out emitting the qualified form; this closes that gap rather than inventing a new convention.
  • The recovery round-trip is sound. getSqlType() now re-qualifies via qualifyDomain in both branches (the getSQLType() path and the dataType()/sqlName fallback), and an already-qualified name (test double / introspected snapshot) passes through unchanged — so buildersByDomain/EQL_V3_DOMAINS lookups on the canonical public.eql_v3_* keys still match. stripDomainSchema (strip leading public.) and qualifyDomain (prefix public. when bare) are exact inverses over the domain set.
  • Good regression guard. The new test asserts every V3_MATRIX domain's getSQLType() is dot-free and equals the bare slug — that's precisely the string drizzle-kit quote-wraps into the migration, so a future re-qualification can't silently reintroduce an un-runnable CREATE/ALTER. Bare-name recovery is covered alongside the existing qualified case.
  • Skill (stash-drizzle) updated to describe the unqualified type and the search-path requirement; @cipherstash/stack-drizzle patch + stash patch changeset present (correct, since the skill ships in the tarball).

Notes — nits, non-blocking

  1. Search-path coupling is now a documented requirement. The bare name only resolves if public is on search_path. That's the Postgres default and matches v2's behaviour, and the skill now spells it out — so this is acceptable, just worth being conscious that a user who drops public from search_path (or installs the eql_v3_* domains in a non-public schema) gets a "type does not exist" at migrate time. EQL_V3_DOMAIN_SCHEMA/stripDomainSchema both hardcode public, consistent with the installer, so no code change is warranted — noting only.

  2. stripDomainSchema strips only public.; qualifyDomain re-qualifies any dot-free name to public. They're inverses because every v3 domain lives in public. If a getEqlType() ever returned a non-public qualified name, stripDomainSchema would pass it through and drizzle-kit would emit the same invalid dotted identifier this PR fixes. Can't happen today (registry is public-only), but a one-line assertion or comment tying the two helpers to that invariant would make the coupling explicit for a future reader.

Verdict

Correct, consistent with the existing v2 and v3-operator conventions, and well-guarded against regression. I'd approve this. Both notes are optional. Say the word and I'll flip this to a formal approval.

…ping schema

`makeEqlV3Column` emits the bare domain name because drizzle-kit wraps a
customType's whole `dataType()` return in one pair of quotes, so a qualified
`public.eql_v3_text_search` would emit the invalid identifier
`"public.eql_v3_text_search"`. Stripping the `public.` schema is only safe
because every domain lives in `public` — `stripDomainSchema`/`qualifyDomain` are
inverses over exactly that set.

If a domain ever lived in another schema, `stripDomainSchema` would pass its
qualified name through untouched and drizzle-kit would silently emit the invalid
dotted identifier again. Assert the invariant at column construction so that day
fails loudly with an actionable message instead of shipping a broken migration.
Addresses the schema-qualification review notes on #688.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
@coderdan

Copy link
Copy Markdown
Contributor Author

Addressed both schema-qualification notes in 4956900.

On the substance: I dug into whether we could honor "always fully qualify" at the dataType() emission, and it's foreclosed by drizzle-kit — it wraps the whole dataType() string in one pair of quotes, so a qualified public.eql_v3_text_search emits the invalid identifier "public.eql_v3_text_search" (the exact bug this PR fixes). The v2 surface already proved the pre-quoted variant double-escapes on the ALTER path (stack-drizzle/src/index.ts:9). So bare-name emission is forced, not a style choice.

What I did change is note 2 — the fragility you flagged. stripDomainSchema/qualifyDomain are inverses only because every domain lives in public; a non-public qualified name would slip through stripDomainSchema untouched and re-introduce the invalid dotted DDL silently. makeEqlV3Column now asserts the public. invariant at construction and throws an actionable error, with a test covering it. Note 1 (search-path coupling) stays as-is — it's the documented, intended behaviour, now guarded so it can't degrade silently.

Also filed #693 for a gap this surfaced beyond the PR's scope: the CLI's rewriteEncryptedAlterColumns is v2-only, so a v3 ALTER COLUMN type change still emits the mangled "undefined"."eql_v3_*" form with no rewriter. Pre-existing, not a regression from this PR.

@coderdan
coderdan merged commit b8cb599 into main Jul 18, 2026
14 of 15 checks passed
@coderdan
coderdan deleted the fix/drizzle-kit-eql-v3-ddl branch July 18, 2026 09:57
@coderdan coderdan mentioned this pull request Jul 18, 2026
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.

2 participants