Skip to content

Simplify the canonical D1 Worker example - #49

Merged
mkuznets merged 4 commits into
mainfrom
simplify-d1-worker-example
Aug 15, 2026
Merged

Simplify the canonical D1 Worker example#49
mkuznets merged 4 commits into
mainfrom
simplify-d1-worker-example

Conversation

@mkuznets

Copy link
Copy Markdown
Owner

The canonical example was spending most of its lines on HTTP plumbing rather than on the generated D1 code it exists to demonstrate. Three commits, each independently green, so the mechanical reformat stays out of the way of the substantive changes.

1. Unify formatting on the repository configuration

examples/d1-worker/ and test/miniflare/ each carried byte-identical copies of .prettierrc, .editorconfig and .gitignore, so one code style was maintained in two places and disagreed with the root in all three. They now inherit the repository configuration — 120 columns, spaces, double quotes.

The win beyond deduplication: those directories hold checked-in plugin output, which has always been 2-space and double-quoted. Under the old split, the example demonstrating the generated code looked foreign sitting next to it.

The .editorconfig copies also carried a latent bug. Their [*.yml] section never matched sqlc.yaml, the only file it was meant to protect, so YAML inherited indent_style = tab — illegal for indentation in YAML. Any editorconfig-aware editor would have written a file sqlc cannot parse.

2. Run the fixture sub-projects on npm

Those two directories were the only part of the repository on bun, for no reason the tree records. Everything else already runs on the pinned Node and npm, so they brought a second package manager, a second lockfile format and a pinned oven-sh/setup-bun along with them. bun.lock becomes package-lock.json, the Makefile targets run npm ci / npm run, and the setup action loses its bun input, install step and version echo.

3. Rewrite the canonical Worker on Hono

Manual URL parsing, a path regex, hand-rolled method branching and a request-wide try/catch are gone. What remains is two route declarations, the query calls, and one onError that keeps the only error handling actually about this project — SqlcD1Error means a generated query failed, anything else does not.

No validation layer replaced parseId and isRenameBody, because the generated runtime already validates its own arguments. A malformed id or missing name raises QueryArgumentError before D1 is called:

request response
GET /users/not-an-id 500 query_failedQueryArgumentError from the generated code
PATCH {"nickname":"…"} 500 query_failed — same
PATCH with malformed JSON 500 internal_error — Hono's parse error, correctly not labelled a query failure

So the malformed-input test changed character rather than disappearing: it now asserts the generated argument checks firing, which is a demonstration of the library instead of a test of hand-written HTTP plumbing.

Trade-offs, deliberate: bad input answers 500 rather than 400 — a real API would validate at the edge, but this one exists to show the query surface. The 405 responses went too; they needed two fallback routes in a specific registration order, and nothing about D1 is clearer for having them.

src/index.ts goes from 46 lines to 35, with one dependency added.

Verification

make clean && make test from a fresh plugin build: 80 unit, 27 candidate, drift clean, 33 miniflare, 5 example. make fmt-check clean repo-wide. wrangler deploy --dry-run bundles the example at 87.93 KiB with the env.DB binding resolved, which exercises the new dependency through wrangler's bundler rather than only vitest's. Commits 1 and 2 were each checked out into a worktree and verified on their own so git bisect never lands on a broken tree. The four generated fixture files are byte-identical — make fmt never touched plugin output.

One thing left open

npm audit reports seven findings that bun never surfaced — esbuild, sharp, undici and ws, all transitive under miniflare, wrangler and vitest-pool-workers. Dev-only and predating this PR. Those lockfiles are a deliberately pinned test environment, so bumping them is a separate decision rather than something to slip in here.

`examples/d1-worker/` and `test/miniflare/` each carried their own
`.prettierrc`, `.editorconfig` and `.gitignore` — byte-identical copies,
so one code style was maintained in two places and disagreed with the
root in all three files. The sub-projects now inherit the repository
configuration: 120 columns, spaces, double quotes.

The hand-written sources in those directories sit beside checked-in
plugin output, which has always been 2-space and double-quoted. Under
the old split the example that demonstrates the generated code looked
foreign next to it.

The `.gitignore` copies were redundant — every pattern already appears in
the root, which additionally ignores `.env*`. The `.editorconfig` copies
also carried a latent bug: their `[*.yml]` section never matched
`sqlc.yaml`, the only file it was meant to protect, so YAML inherited
`indent_style = tab`. Tabs are illegal for indentation in YAML, and any
editorconfig-aware editor would have written a file sqlc cannot parse.
`examples/d1-worker/` and `test/miniflare/` were the only parts of the
repository on bun, for no reason the tree records. Everything else —
the root package, CI, the release path — already runs on the pinned
Node and npm, so the two fixtures brought a second package manager, a
second lockfile format and a pinned `oven-sh/setup-bun` along for the
ride.

`bun.lock` becomes `package-lock.json` in both, `make test-miniflare`
and `make test-example` run `npm ci` and `npm run`, and the setup action
loses its `bun` input, its install step and its version echo. Nothing
outside those two directories consumed bun.

`examples/d1-worker/CLAUDE.md` existed solely to tell an agent to reach
for bun there; npm needs no such instruction. The root `.prettierignore`
already ignores `package-lock.json` at any depth, so the two `bun.lock`
entries leave with nothing to replace them.

`npm audit` reports seven findings that bun never surfaced — esbuild,
sharp, undici and ws, all transitive under miniflare, wrangler and
vitest-pool-workers. They are dev-only and predate this change; the
Cloudflare versions in these lockfiles are a deliberately pinned test
environment, so bumping them is a separate decision.
The example exists to show what consuming the generated D1 code looks
like, and most of it was showing something else: manual `URL` parsing, a
path regex, method branching by hand, and a `try`/`catch` wrapped around
the whole request. Routing plumbing outnumbered the query calls it was
supposed to frame.

Hono replaces all of it. `app.get`/`app.patch` declare the two paths,
`c.req.param` supplies the id, and `app.onError` keeps the one piece of
error handling that is actually about this project: `SqlcD1Error` means
a generated query failed, anything else does not.

`parseId` and `isRenameBody` are gone with no validation layer put in
their place, because the generated runtime already validates its own
arguments. A malformed id or a missing name raises `QueryArgumentError`
before D1 is called, so the test that used to assert hand-written 400s
now asserts the generated argument checks firing — a demonstration of
the library rather than of HTTP plumbing. The trade is that bad input
answers 500 rather than 400; a real API would validate at the edge, but
this one is here to show the query surface.

The 405 responses went too. They needed two fallback routes registered
in a specific order, and nothing about D1 is clearer for having them;
an unhandled method now gets Hono's 404.

Net effect on the file: 46 lines to 35, one dependency, and every
remaining line is a query call, a session, or the D1 binding.
The matrix mirrors each fixture without `node_modules` and type-checks it
with the root TypeScript, which held while every hand-written fixture
source imported nothing. The Worker now imports Hono, so both cells
failed on `Cannot find module 'hono'` — a dependency the matrix has no
reason to resolve.

Excluding the hand-written files is not sufficient on its own: `wrangler
types` writes `mainModule: typeof import("./src/index")` into
worker-configuration.d.ts, and TypeScript cannot exclude a file reached
through a reference, so the entry point comes back through the type
declarations. The matrix therefore includes the generated directory
alone. D1 types already come from the root `@cloudflare/workers-types`
that this script pins into `compilerOptions.types`.

This narrows the matrix to the question it exists to answer — does the
plugin's output compile under each supported sqlc version — and it still
compiles both emitted files. The hand-written sources keep their
type-check in `make test-example` and `make test-miniflare`, which run
against real installs.
@mkuznets
mkuznets merged commit e8687dd into main Aug 15, 2026
3 checks passed
@mkuznets
mkuznets deleted the simplify-d1-worker-example branch August 15, 2026 22:58
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