Background
EQL (Encrypt Query Language) is the SQL library the CLI installs into the customer's database as the eql_v3 schema; encrypted columns use eql_v3_* domain types. For Drizzle (a TypeScript ORM) projects, stash eql migration --drizzle is the supported way to get the EQL install SQL into the project's own migration history: it scaffolds a custom migration through the project's drizzle-kit (so drizzle-kit keeps owning the journal and sequence numbers) and then writes/rewrites the SQL itself (src/commands/db/rewrite-migrations.ts). That rewrite step exists because drizzle-kit generate on its own emits broken DDL for encrypted columns (see Problem, second half).
Problem
Part 1 — the supported path aborts. src/commands/eql/migration.ts:416 spawns the project-local drizzle-kit with spawnSync(command, drizzleArgs, { stdio: 'pipe', encoding: 'utf-8' }) — no dotenv loading of the project's .env / .env.local beforehand. The dominant Drizzle project layout (including every Supabase starter) keeps DATABASE_URL in .env.local and wraps drizzle-kit in npm scripts like dotenv -e .env.local -- drizzle-kit …; drizzle.config.ts reads process.env.DATABASE_URL. Invoked directly by stash, that wrapper never runs, the config sees undefined, drizzle-kit exits non-zero, and the command aborts ("Migration aborted.", migration.ts:420-432). Verified in the 2026-08-19 skilltester run on the vite-drizzle surface (branch 20260819-05-claude, stash@1.1.0 + @cipherstash/stack-drizzle@1.1.0).
Part 1b — a second, independent way the same spawn fails. On drizzle-kit 0.31.x the command also dies even with a reachable DATABASE_URL: stash passes an absolute --out=<path>, which that drizzle-kit release ignores/mishandles, so the scaffold step fails on every 0.31.x project (2026-08-19 skilltester, next-drizzle surface, branch 20260819-09-claude). Either failure is then hidden by the same misleading abort message — the CLI prints "Make sure drizzle-kit is installed and configured" instead of drizzle-kit's actual stderr (captured but summarized away, migration.ts:420-432).
Part 2 — the fallback the user lands on is the bug the command was built to bury. With the supported path dead, the natural next move is plain drizzle-kit generate. On the alter path (existing plaintext column → encrypted domain type), that emits un-runnable DDL:
ALTER TABLE "transactions" ALTER COLUMN "description" SET DATA TYPE "undefined"."eql_v3_text_search";
The "undefined" is JavaScript's undefined leaking into an SQL identifier: @cipherstash/stack-drizzle deliberately returns the bare domain name from its customType.dataType() (src/column.ts:12,24,141-142 — with a comment noting drizzle-kit cannot render schema-qualified custom types as valid DDL) and schema-qualifies at runtime instead; drizzle-kit's ALTER renderer then prints the snapshot's missing typeSchema literally. There is also no USING-cast from the plaintext type, so even a hand-corrected schema name would fail. The agent in the vite-drizzle run recovered only by writing a destructive DROP+ADD custom migration (safe on greenfield, data-loss on a real table).
Impact: every Drizzle user whose DATABASE_URL lives in a dotenv file — the default for Supabase-style projects — loses the entire supported EQL-install path, and the failure funnels them directly into broken or destructive DDL. Nothing catches it: the abort message suggests checking that drizzle-kit is installed, which is not the problem.
Proposal
- Interim (cheap): before spawning drizzle-kit, load the project's
.env and .env.local into the child env (the CLI already resolves a database URL for its own connection — thread that same resolved value through, e.g. spawnSync(..., { env: { ...process.env, DATABASE_URL: resolvedUrl } })).
- Make the abort message name the actual failure: print drizzle-kit's stderr (already captured) plus a hint that
drizzle.config.ts could not see DATABASE_URL when that is detectable.
- Longer term: give
stash eql repair --drizzle (or the migration rewriter) a same-name, cast-preserving recipe for the alter path (ALTER … SET DATA TYPE public.eql_v3_* USING <cast>), so even users who bypass the CLI have a non-destructive route. Today's repair only offers an add-twin *_encrypted rewrite that renames the column.
Evidence
- vite-drizzle assessment, cipherstash/skilltester branch
20260819-05-claude (apps/vite-drizzle/.cipherstash/assessment.md, gaps 2-3): both failures reproduced by the independent assessor against live drizzle-kit.
- Published-tarball source refs above (
stash@1.1.0, @cipherstash/stack-drizzle@1.1.0 sourcemaps). Part 1's mechanism (missing dotenv preload) is verified; whether an explicitly exported DATABASE_URL in the parent shell also fails to reach the child (as the assessor reported) is plausible but not independently re-verified — spawnSync defaults inherit the parent env, so the dotenv gap is the confirmed path.
Relationship to other work
Background
EQL (Encrypt Query Language) is the SQL library the CLI installs into the customer's database as the
eql_v3schema; encrypted columns useeql_v3_*domain types. For Drizzle (a TypeScript ORM) projects,stash eql migration --drizzleis the supported way to get the EQL install SQL into the project's own migration history: it scaffolds a custom migration through the project'sdrizzle-kit(so drizzle-kit keeps owning the journal and sequence numbers) and then writes/rewrites the SQL itself (src/commands/db/rewrite-migrations.ts). That rewrite step exists becausedrizzle-kit generateon its own emits broken DDL for encrypted columns (see Problem, second half).Problem
Part 1 — the supported path aborts.
src/commands/eql/migration.ts:416spawns the project-localdrizzle-kitwithspawnSync(command, drizzleArgs, { stdio: 'pipe', encoding: 'utf-8' })— no dotenv loading of the project's.env/.env.localbeforehand. The dominant Drizzle project layout (including every Supabase starter) keepsDATABASE_URLin.env.localand wraps drizzle-kit in npm scripts likedotenv -e .env.local -- drizzle-kit …;drizzle.config.tsreadsprocess.env.DATABASE_URL. Invoked directly bystash, that wrapper never runs, the config seesundefined, drizzle-kit exits non-zero, and the command aborts ("Migration aborted.",migration.ts:420-432). Verified in the 2026-08-19 skilltester run on thevite-drizzlesurface (branch20260819-05-claude,stash@1.1.0+@cipherstash/stack-drizzle@1.1.0).Part 1b — a second, independent way the same spawn fails. On drizzle-kit 0.31.x the command also dies even with a reachable
DATABASE_URL:stashpasses an absolute--out=<path>, which that drizzle-kit release ignores/mishandles, so the scaffold step fails on every 0.31.x project (2026-08-19 skilltester,next-drizzlesurface, branch20260819-09-claude). Either failure is then hidden by the same misleading abort message — the CLI prints "Make sure drizzle-kit is installed and configured" instead of drizzle-kit's actual stderr (captured but summarized away,migration.ts:420-432).Part 2 — the fallback the user lands on is the bug the command was built to bury. With the supported path dead, the natural next move is plain
drizzle-kit generate. On the alter path (existing plaintext column → encrypted domain type), that emits un-runnable DDL:The
"undefined"is JavaScript'sundefinedleaking into an SQL identifier:@cipherstash/stack-drizzledeliberately returns the bare domain name from itscustomType.dataType()(src/column.ts:12,24,141-142— with a comment noting drizzle-kit cannot render schema-qualified custom types as valid DDL) and schema-qualifies at runtime instead; drizzle-kit's ALTER renderer then prints the snapshot's missingtypeSchemaliterally. There is also noUSING-cast from the plaintext type, so even a hand-corrected schema name would fail. The agent in the vite-drizzle run recovered only by writing a destructive DROP+ADD custom migration (safe on greenfield, data-loss on a real table).Impact: every Drizzle user whose
DATABASE_URLlives in a dotenv file — the default for Supabase-style projects — loses the entire supported EQL-install path, and the failure funnels them directly into broken or destructive DDL. Nothing catches it: the abort message suggests checking that drizzle-kit is installed, which is not the problem.Proposal
.envand.env.localinto the child env (the CLI already resolves a database URL for its own connection — thread that same resolved value through, e.g.spawnSync(..., { env: { ...process.env, DATABASE_URL: resolvedUrl } })).drizzle.config.tscould not seeDATABASE_URLwhen that is detectable.stash eql repair --drizzle(or the migration rewriter) a same-name, cast-preserving recipe for the alter path (ALTER … SET DATA TYPE public.eql_v3_* USING <cast>), so even users who bypass the CLI have a non-destructive route. Today's repair only offers an add-twin*_encryptedrewrite that renames the column.Evidence
20260819-05-claude(apps/vite-drizzle/.cipherstash/assessment.md, gaps 2-3): both failures reproduced by the independent assessor against live drizzle-kit.stash@1.1.0,@cipherstash/stack-drizzle@1.1.0sourcemaps). Part 1's mechanism (missing dotenv preload) is verified; whether an explicitlyexportedDATABASE_URLin the parent shell also fails to reach the child (as the assessor reported) is plausible but not independently re-verified —spawnSyncdefaults inherit the parent env, so the dotenv gap is the confirmed path.Relationship to other work
supabase db reset#613 tracks--eql-version 3rejection on--drizzle; stash init/plan rough edges surfaced by the rc.1 skilltester run (umbrella) #665 item 1 tracks version-flag handling on the same command — different defects on the same code path.