Skip to content

fix(drizzle): emit bare eql_v2_encrypted and rewrite all mangled ALTER forms#353

Draft
coderdan wants to merge 1 commit intomainfrom
fix/drizzle-encrypted-datatype
Draft

fix(drizzle): emit bare eql_v2_encrypted and rewrite all mangled ALTER forms#353
coderdan wants to merge 1 commit intomainfrom
fix/drizzle-encrypted-datatype

Conversation

@coderdan
Copy link
Copy Markdown
Contributor

@coderdan coderdan commented Apr 19, 2026

Summary

@cipherstash/stack@0.15.0 + @cipherstash/cli@0.6.0 produce invalid ALTER COLUMN migrations when npx drizzle-kit generate converts an existing column to encryptedType, e.g.:

ALTER TABLE "transactions" ALTER COLUMN "amount" SET DATA TYPE "undefined".""public"."eql_v2_encrypted"";

Two bugs combine to produce this:

  1. EQL_ENCRYPTED_DATA_TYPE = '"public"."eql_v2_encrypted"' (packages/stack/src/drizzle/index.ts). drizzle-kit's ALTER COLUMN path wraps the dataType() return value in double-quotes and unconditionally prepends "{typeSchema}".. Custom types have no typeSchema, so the pre-quoted string came out as "undefined".""public"."eql_v2_encrypted"". (The CREATE TABLE path behaves correctly — it skips the prefix when typeSchema is empty — which is why this regression shipped.)
  2. ALTER_COLUMN_TO_ENCRYPTED_RE (packages/cli/src/commands/db/rewrite-migrations.ts) only matched eql_v2_encrypted and "public"."eql_v2_encrypted", so neither the pre-0.15.0 "undefined"."eql_v2_encrypted" form nor the 0.15.0 "undefined".""public"."eql_v2_encrypted"" form triggered the ADD + DROP + RENAME rewrite. Broken ALTERs survived into the final migration.

Changes

  • encryptedType now returns the bare eql_v2_encrypted identifier from customType.dataType(). drizzle-kit's CREATE TABLE path already omits the schema prefix when typeSchema is empty, so this emits correctly there. ALTER COLUMN output is still mangled by drizzle-kit, but the rewriter now catches it.
  • rewriteEncryptedAlterColumns regex broadened to match all four forms drizzle-kit may emit. Each is rewritten into the safe ADD COLUMN … DROP COLUMN … RENAME COLUMN sequence.
  • getEncryptedColumnConfig continues to accept both sqlName forms for back-compat with schemas built against 0.15.0.
  • Added two new tests in rewrite-migrations.test.ts covering the "undefined"."eql_v2_encrypted" and "undefined".""public"."eql_v2_encrypted"" forms.
  • Changeset: patch bumps for @cipherstash/stack and @cipherstash/cli.

Test plan

  • pnpm --filter @cipherstash/cli test — 126 passed (incl. 2 new rewriter tests)
  • pnpm --filter @cipherstash/stack exec vitest run __tests__/drizzle-operators-jsonb.test.ts — 11 passed
  • Regenerate a drizzle migration against a Next.js + Drizzle spike that encrypts existing columns; confirm the output is ADD COLUMN "x__cipherstash_tmp" "public"."eql_v2_encrypted"; + DROP + RENAME with no "undefined" anywhere
  • Run the wizard end-to-end on the same spike and confirm the generated migration applies cleanly

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Resolved database migration handling for encrypted columns when external tools generate malformed type references
    • Improved migration rewriting to recognize and safely normalize variant encrypted type identifiers

…forms

@cipherstash/stack 0.15.0 returned `"public"."eql_v2_encrypted"` from the
Drizzle customType `dataType()` callback, aiming to avoid
`"undefined"."eql_v2_encrypted"` in drizzle-kit's ALTER COLUMN output. But
drizzle-kit's ALTER COLUMN path wraps the return value in double-quotes and
unconditionally prepends `"{typeSchema}".`, and custom types have no
typeSchema — so the pre-quoted form came out as
`"undefined".""public"."eql_v2_encrypted""`, which Postgres can't parse.
The CLI's migration rewriter regex matched neither that form nor the
original `"undefined"."eql_v2_encrypted"` form, so broken ALTER statements
survived into the final migration.

- Return the bare `eql_v2_encrypted` identifier. drizzle-kit's CREATE TABLE
  path already omits the schema prefix when typeSchema is empty, so this
  emits correctly there.
- Broaden rewriteEncryptedAlterColumns' regex to match all four forms
  drizzle-kit may produce. The rewriter converts each into the safe
  ADD COLUMN + DROP COLUMN + RENAME sequence.
- getEncryptedColumnConfig still accepts both the bare and the pre-quoted
  sqlName values for back-compat with tables built against 0.15.0.
- Add tests for the two newly-matched forms.
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 19, 2026

🦋 Changeset detected

Latest commit: 8513705

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

This PR includes changesets to release 3 packages
Name Type
@cipherstash/stack Patch
@cipherstash/cli Patch
@cipherstash/basic-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

@coderdan coderdan marked this pull request as draft April 19, 2026 07:12
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

The changes fix how the eql_v2_encrypted custom type is handled across Drizzle-related migrations by updating the encrypted type identifier from a pre-quoted, schema-qualified form to a bare identifier, and expanding migration rewriting logic to recognize and normalize various malformed type reference variants.

Changes

Cohort / File(s) Summary
Changesets Documentation
.changeset/fix-drizzle-encrypted-datatype.md
Documents patch releases with fixes for eql_v2_encrypted handling in Drizzle migrations, including updated encryptedType to return unqualified identifier and migration rewriting to handle variant type reference forms.
Test Coverage
packages/cli/src/__tests__/rewrite-migrations.test.ts
Adds two test cases validating rewriteEncryptedAlterColumns behavior for schema-qualified ("undefined"."eql_v2_encrypted") and double-quoted malformed type references, ensuring proper normalization.
Migration Rewriting
packages/cli/src/commands/db/rewrite-migrations.ts
Updates ALTER_COLUMN_TO_ENCRYPTED_RE regex to match additional drizzle-kit output variants including "undefined" schema qualifiers and double-quoted forms alongside original plain and fully-qualified patterns.
Drizzle Type Definition
packages/stack/src/drizzle/index.ts
Changes encrypted type identifier constant from "public"."eql_v2_encrypted" to bare eql_v2_encrypted and updates detection logic to recognize both current and legacy type reference forms.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • calvinbrewer
  • tobyhede

Poem

🐰 A type that was lost in the schema's great dance,
Now caught and reclaimed with a RegEx enhance!
From quoted confusion to patterns so keen,
The clearest eql_v2 we've ever seen!
Drizzle and Cipherstash, in harmony bound,
Where migrations run smooth without making a sound! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: changing drizzle to emit a bare identifier and rewriting malformed ALTER forms, which are the core issues addressed in the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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-encrypted-datatype

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/cli/src/__tests__/rewrite-migrations.test.ts (1)

54-82: Good variant coverage.

Both new tests pin the exact mangled forms drizzle-kit emits and assert the full ADD+DROP+RENAME shape plus absence of SET DATA TYPE, which is the right contract. Consider also adding a negative-ish assertion — e.g. expect(updated).not.toContain('"undefined"') — to guard against a future regex regression that keeps the original (mangled) statement intact while still matching ADD COLUMN. Optional, not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/__tests__/rewrite-migrations.test.ts` around lines 54 - 82,
Add a negative assertion in each test to ensure the mangled namespace is fully
removed: after calling rewriteEncryptedAlterColumns(...) and reading the updated
file into the updated variable, add expect(updated).not.toContain('"undefined"')
(or similar check for the exact mangled substring) in both tests ("rewrites the
\"undefined\" schema form drizzle-kit emits for bare custom types" and "rewrites
the double-quoted form produced by stack 0.15.0") so the regex change cannot
accidentally leave the original '"undefined"' text in the output while still
producing the ADD COLUMN sequence.
packages/cli/src/commands/db/rewrite-migrations.ts (1)

21-22: Regex alternation ordering is correct, but the bare eql_v2_encrypted branch is unanchored.

Alternation ordering is fine (longer/more-specific "undefined".""public"... comes first, so it's not shadowed by the shorter "undefined"."eql_v2_encrypted" branch). However, the final eql_v2_encrypted alternative has no right-side boundary, so a hypothetical SET DATA TYPE eql_v2_encrypted_v2; (or any suffix) would also match and be rewritten. Unlikely today, but cheap to harden:

Optional: anchor the bare form with a non-identifier lookahead
-  /ALTER TABLE "([^"]+)"\s+ALTER COLUMN "([^"]+)"\s+SET DATA TYPE (?:"undefined"\.""public"\."eql_v2_encrypted""|"undefined"\."eql_v2_encrypted"|"public"\."eql_v2_encrypted"|eql_v2_encrypted)[^;]*;/gi
+  /ALTER TABLE "([^"]+)"\s+ALTER COLUMN "([^"]+)"\s+SET DATA TYPE (?:"undefined"\.""public"\."eql_v2_encrypted""|"undefined"\."eql_v2_encrypted"|"public"\."eql_v2_encrypted"|eql_v2_encrypted\b)[^;]*;/gi

Also worth noting: [^;]*; greedily consumes anything up to the next ;, which is fine for single-statement lines but would over-match if drizzle-kit ever emitted trailing clauses with embedded semicolons in string literals. Not a realistic concern for this path — flagging for awareness only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/db/rewrite-migrations.ts` around lines 21 - 22, The
final bare alternative in ALTER_COLUMN_TO_ENCRYPTED_RE is unanchored and will
wrongly match longer identifiers like eql_v2_encrypted_v2; update the regex
(ALTER_COLUMN_TO_ENCRYPTED_RE) so the bare eql_v2_encrypted branch is anchored
with a non-identifier lookahead (e.g., a word-boundary or (?=\W|;)) to ensure
only the exact type name matches, leaving the other alternates unchanged; locate
ALTER_COLUMN_TO_ENCRYPTED_RE in rewrite-migrations.ts and replace the last
alternation accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/cli/src/__tests__/rewrite-migrations.test.ts`:
- Around line 54-82: Add a negative assertion in each test to ensure the mangled
namespace is fully removed: after calling rewriteEncryptedAlterColumns(...) and
reading the updated file into the updated variable, add
expect(updated).not.toContain('"undefined"') (or similar check for the exact
mangled substring) in both tests ("rewrites the \"undefined\" schema form
drizzle-kit emits for bare custom types" and "rewrites the double-quoted form
produced by stack 0.15.0") so the regex change cannot accidentally leave the
original '"undefined"' text in the output while still producing the ADD COLUMN
sequence.

In `@packages/cli/src/commands/db/rewrite-migrations.ts`:
- Around line 21-22: The final bare alternative in ALTER_COLUMN_TO_ENCRYPTED_RE
is unanchored and will wrongly match longer identifiers like
eql_v2_encrypted_v2; update the regex (ALTER_COLUMN_TO_ENCRYPTED_RE) so the bare
eql_v2_encrypted branch is anchored with a non-identifier lookahead (e.g., a
word-boundary or (?=\W|;)) to ensure only the exact type name matches, leaving
the other alternates unchanged; locate ALTER_COLUMN_TO_ENCRYPTED_RE in
rewrite-migrations.ts and replace the last alternation accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75c8d8d3-58d0-4589-bc7a-181dc225900d

📥 Commits

Reviewing files that changed from the base of the PR and between 0e8d960 and 8513705.

📒 Files selected for processing (4)
  • .changeset/fix-drizzle-encrypted-datatype.md
  • packages/cli/src/__tests__/rewrite-migrations.test.ts
  • packages/cli/src/commands/db/rewrite-migrations.ts
  • packages/stack/src/drizzle/index.ts

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.

1 participant