Skip to content

feat: MERGE and EXPLAIN can be run - #196

Merged
productdevbook merged 1 commit into
mainfrom
feat/merge-and-explain-run
Aug 21, 2026
Merged

productdevbook merged 1 commit into
mainfrom
feat/merge-and-explain-run

Conversation

@productdevbook

Copy link
Copy Markdown
Owner

Two public entry points produced queries nobody could run.

db.mergeInto(...) built a query with no executor behind it — no .run(), no .many(), and a compiled MERGE rejected. Callers had to take the SQL and drive the connection themselves, which is the one thing the rest of the API does for them.

.explain() was the same: a plan you could compile and not read, which is the only reason to ask for one.

What they get

MERGE gets .run(), and its RETURNING form gets .many() / .first() / .run().

EXPLAIN gets .many(), returning what the engine prints as Record<string, unknown>[] rather than a shape that would only be right for one dialect. Result plugins are deliberately not applied — a query plan is not a row of your table.

Both are tested against pglite, including the compiled MERGE running and updating the row.

The check

every-builder-compiles.test.ts grows a second assertion: no builder that emits SQL may be unable to run. Like the first, it reads src/builder rather than a list, because the hole appears when somebody adds a builder, not when they add a test.

Audited before and after:

                          before      after
ExplainBuilder            no run      runs
TypedMergeBuilder         no run      runs
TypedMergeReturningB.     no run      runs
(the other seven)         runs        runs

pnpm test green: 3322 tests, lint clean, types clean.

`db.mergeInto(...)` was a public entry point producing a query nobody could
run. The builder carried no executor, so there was no `.run()`, no `.many()`,
and a compiled MERGE rejected — callers had to take the SQL and drive the
connection themselves, which is the one thing the rest of the API does for
them. `.explain()` was the same: a plan you could compile and not read,
which is the only reason to ask for one.

Both now carry the instance they were built from. MERGE gets `.run()`, and
its RETURNING form gets `.many()` / `.first()` / `.run()`; EXPLAIN gets
`.many()`, returning whatever the engine prints as
`Record<string, unknown>[]` rather than a shape that would only be right for
one dialect. Result plugins are deliberately not applied to a plan — it is
not a row of your table.

`every-builder-compiles.test.ts` grows a second check: no builder that emits
SQL may be unable to run. Like the first, it reads `src/builder` rather than
a list, because the hole appears when somebody adds a builder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HAwiBVhLmNhWjC9Ro6UMpb
@productdevbook
productdevbook merged commit 261f488 into main Aug 21, 2026
1 check passed
@productdevbook
productdevbook deleted the feat/merge-and-explain-run branch August 21, 2026 17:24
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