Skip to content

fix(cli): make eql migration --drizzle actually run drizzle-kit - #927

Open
coderdan wants to merge 2 commits into
mainfrom
fix/924-eql-migration-drizzle-kit-spawn
Open

fix(cli): make eql migration --drizzle actually run drizzle-kit#927
coderdan wants to merge 2 commits into
mainfrom
fix/924-eql-migration-drizzle-kit-spawn

Conversation

@coderdan

Copy link
Copy Markdown
Contributor

Fixes #924.

The defect is broader than the issue reports

stash eql migration --drizzle has never worked for any project with a drizzle.config.ts — the --out failure is not drizzle-kit 0.31.x-specific. Reproduced identically on drizzle-kit 0.28.5, 0.30.6 and 0.31.4:

Error  Please provide required params:
    [x] schema: undefined
    [x] dialect: undefined
    [✓] out: '/abs/path/drizzle'

generate reads its config file or its command-line options, never both. Passing any of --schema/--out/--dialect switches it out of config-file mode, and it then demands all three. --config cannot be combined with them either (You can't use both --config and other cli options for generate command).

And drizzle-kit writes those errors to stdout, not stderr. The reporter only read result.stderr, so the user got an empty error line followed by "Make sure drizzle-kit is installed and configured" — the one thing that was never wrong. That is why the real cause stayed invisible in the skilltester run.

What changed

1. --out no longer reaches drizzle-kit. drizzle.config.ts decides the output directory; we read back the path drizzle-kit reports on stdout ([✓] Your SQL migration file ➜ drizzle/0000_install-eql.sql 🚀) and warn when it differs from an --out the user passed. --out remains the fallback directory to scan when no path is reported, so a drizzle-kit whose banner we do not recognise still works. The ALTER COLUMN sweep and the closing note now operate on the directory the file actually landed in, not the one we guessed.

2. The resolved DATABASE_URL is threaded into the child env. A drizzle.config.ts reading process.env.DATABASE_URL (and often throwing when it is missing) is the dominant layout, wrapped in dotenv -e .env.local -- drizzle-kit … npm scripts that never run when stash invokes drizzle-kit directly. bin/main.ts already loads .env/.env.local at startup; on top of that we now pass down a URL only this CLI can find — a running local Supabase — via a new tryResolveDatabaseUrl(): same sources as resolveDatabaseUrl, minus the interactive prompt and the hard exit, because a missing URL is an ordinary answer when decorating a child's environment.

3. Both streams are reported. A config that could not read DATABASE_URL gets a follow-up naming the detected dotenv file; anything else gets a reproduce-it-directly line instead of the install advice. stash init --drizzle's equivalent failure hint was fixed the same way.

Verification

End-to-end against real drizzle-kit 0.31.4, in a project whose drizzle.config.ts throws on a missing DATABASE_URL supplied only via .env.local — the exact reported scenario. Generates and writes the install SQL; the --out divergence warning fires when the config writes elsewhere.

Unit coverage added for each defect: --out absent from the argv, stdout surfaced on failure, both streams joined, the URL threaded (with process.env.DATABASE_URL deleted, so inheritance alone would fail), env left untouched when nothing resolves, the reported path followed outside --out, and the scan fallback. Plus five cases for tryResolveDatabaseUrl.

packages/cli unit suite: 1355 passing. Two doctor e2e tests fail — pre-existing, confirmed identical on a clean tree (@cipherstash/auth 0.42 platform binary), untouched by this PR.

Proposal 3 in the issue is not implementable as written

The issue's third item asks for a same-name, cast-preserving ALTER … SET DATA TYPE public.eql_v3_* USING <cast> recipe. No such cast can exist: EQL values are encrypted client-side through ZeroKMS, and the bundle ships exactly one cast (eql_v3_json_search AS eql_v3.query_json) — nothing from text/numeric. There is no SQL expression that converts plaintext into an EQL payload. The existing add-twin rewrite plus the staged stash encrypt backfill is the correct shape, and that bullet is worth closing rather than leaving open as work.

Skills

skills/stash-cli (--out and --drizzle flag rows) and skills/stash-drizzle (a paragraph on who owns the output directory and why the dotenv wrapper is not needed). Command surface is unchanged — stash manifest --json still matches.

`stash eql migration --drizzle` aborted for every project with a
drizzle.config.ts, and the abort blamed the one thing that was never
wrong ("Make sure drizzle-kit is installed and configured"). Three
independent defects, all on the spawn (#924):

1. We always passed `--out`. drizzle-kit's `generate` reads its config
   file OR its command-line options, never both: any of --schema/--out/
   --dialect switches it into CLI mode, where it then aborts demanding
   the two we cannot supply ("Please provide required params: [x] schema
   [x] dialect"). `--config` cannot be combined with CLI options either.
   Verified against drizzle-kit 0.28.5, 0.30.6 and 0.31.4 — this was
   never version-specific.

   drizzle.config.ts now decides the output directory, and we follow the
   path drizzle-kit reports on stdout, warning when it differs from a
   `--out` the user passed. `--out` stays the fallback directory to scan
   when no path is reported, so an unrecognised banner still works. The
   sweep and the closing note now use the directory the file actually
   landed in.

2. A drizzle.config.ts reading `process.env.DATABASE_URL` — the
   dominant layout, wrapped in `dotenv -e .env.local -- drizzle-kit …`
   npm scripts we bypass — could see nothing. We already load .env/
   .env.local at startup, and now also thread down a URL only the CLI
   can find (a running local Supabase) via a new non-blocking
   `tryResolveDatabaseUrl`: same sources as `resolveDatabaseUrl` minus
   the prompt and the hard exit.

3. drizzle-kit writes its errors to stdout, not stderr, so the reporter
   printed an empty message. Both streams are now surfaced, and a config
   that could not read DATABASE_URL gets a follow-up naming that.

Verified end-to-end against real drizzle-kit 0.31.4 with a config that
throws on a missing DATABASE_URL supplied only via .env.local.
@coderdan
coderdan requested a review from a team as a code owner August 19, 2026 12:57
@changeset-bot

changeset-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e945d5c

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

This PR includes changesets to release 11 packages
Name Type
stash Patch
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/stack Patch
@cipherstash/stack-drizzle Patch
@cipherstash/stack-supabase Patch
@cipherstash/stack-prisma Patch
@cipherstash/wizard Patch
@cipherstash/bench Patch
@cipherstash/test-kit Patch
@cipherstash/prisma-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

// this CLI can find (a running local Supabase), so the config sees one in
// that case too (#924).
s.start('Generating custom Drizzle migration...')
const databaseUrl = tryResolveDatabaseUrl({ supabase: options.supabase })

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P1] Thread init's resolved URL into drizzle-kit

When stash init --drizzle obtains the URL from its prompt, it stores it only in state.databaseUrl; resolveDatabaseUrl deliberately does not mutate process.env. This resolver therefore returns nothing, so a drizzle.config.ts reading DATABASE_URL still aborts. Pass the init-resolved URL through EqlMigrationOptions and use it in the child environment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in e945d5c. EqlMigrationOptions gains a databaseUrl seam — the same one installCommand already takes — and generateEqlMigration threads state.databaseUrl through it. It takes precedence over the resolver, since a caller-supplied URL is more specific than anything we could rediscover.

// Last match wins: one invocation writes one file, but a wrapper that echoes
// its own banner first should not shadow drizzle-kit's.
let found: string | undefined
for (const match of stdout.matchAll(/➜\s*(\S+\.sql)/g)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P2] Parse reported migration paths containing spaces

When drizzle.config.ts uses an output path containing spaces, \S+ truncates the reported filename and rejects it as nonexistent. The fallback then scans the unrelated --out directory, causing an abort or potentially selecting an older same-name migration. Capture the complete path up to the drizzle-kit banner suffix instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in e945d5c. Now scans per line and takes everything between the arrow and the last .sql on it, so spaces survive; the existence check still gates the result. Verified live against drizzle-kit 0.31.4 with an out of ./my migrations — found and written correctly.

)
p.log.info(
`Make sure drizzle-kit is installed and configured: ${execCommand(pm)} drizzle-kit --version`,
/DATABASE_URL/.test(output)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P2] Do not classify every DATABASE_URL error as missing

Any failure mentioning DATABASE_URL, including malformed URLs or unsupported schemes, is labeled as an unset variable. This replaces drizzle-kit's accurate diagnosis with contradictory remediation. Match only known missing-variable messages or retain the generic follow-up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in e945d5c. New looksLikeMissingDatabaseUrl matches only absence phrasings, and anything unrecognised falls through to the generic follow-up — never wrong, since drizzle-kit output prints directly above it. Bare undefined is deliberately not a trigger, so invalid connection string for DATABASE_URL: undefined scheme stays generic. Table-driven test covers both directions.

Three defects from the PR #927 review, all on the same path:

- P1: `stash init --drizzle` resolves a database URL at the start of the
  run and keeps it in `InitState` — `resolveDatabaseUrl` deliberately
  never writes to `process.env`. The embedded migration route therefore
  passed nothing down, and a drizzle.config.ts reading DATABASE_URL
  still aborted. `EqlMigrationOptions` gains a `databaseUrl` seam (the
  one `installCommand` already has) and init threads its URL through it.

- P2: `parseReportedMigrationPath` matched `\S+\.sql`, truncating a
  reported path at the first space. A drizzle.config.ts writing into a
  directory with a space in its name failed the existence check and fell
  through to scanning an unrelated `--out` — an abort, or an older
  same-named migration. It now scans per line, taking everything between
  the arrow and the last `.sql` on it. Verified live against drizzle-kit
  0.31.4 with an `out` of `./my migrations`.

- P3: every failure mentioning DATABASE_URL was reported as "it is not
  set", which contradicts drizzle-kit whenever the variable is present
  and merely wrong (malformed URL, unsupported scheme, auth failure).
  `looksLikeMissingDatabaseUrl` matches only absence phrasings, and an
  unrecognised one falls through to the generic follow-up — never wrong,
  since drizzle-kit's own output prints directly above it.
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.

stash eql migration --drizzle aborts when DATABASE_URL lives in .env.local — stranding users on drizzle-kit's broken "undefined"."eql_v3_*" DDL

1 participant