8000 feat(sqlite): compile each statement on cloudflare d1 through wrangler by colinhacks · Pull Request #44 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

feat(sqlite): compile each statement on cloudflare d1 through wrangler - #44

Merged
colinhacks merged 3 commits into
mainfrom
d1-statement-check
Oct 22, 2026
Merged

colinhacks merged 3 commits into
mainfrom
d1-statement-check

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

For a project whose package.json lists wrangler and whose wrangler config binds a D1 database, the type check now compiles each SQLite statement on Cloudflare D1 as that wrangler runs it locally. A statement D1 refuses is a reylo6 error at the call:

queries/power.sql(1,12): error reylo6: Cloudflare D1, as wrangler 4.151.3 runs it locally, refuses this statement: not authorized to use function: power

Why. D1 refuses functions its SQLite has, through an authorizer that runs while SQLite compiles a statement, and it refuses pragma_function_list too, so nothing can list what it allows. Probing every function node:sqlite has through local D1, with EXPLAIN, found 127 allowed (all on the allowlist in Cloudflare's runtime source, ALLOWED_SQLITE_FUNCTIONS in workerd's src/workerd/util/sqlite.c++), 22 refused, among them json_pretty, unistr and the aliases ceiling, log10 and power where ceil, log and pow are allowed, and 21 absent from D1's build (FTS3 and FTS4, Geopoly, percentile). All of these passed the type check, which reads the local D1 file through node:sqlite, and failed in the Worker.

How.

  • src/d1-proxy.ts runs in a child process, because wrangler and workerd write to stdout, the content mapper's channel to TypeScript. It calls wrangler's getPlatformProxy({ persist: false }), takes the config's first D1 binding (in memory, so nothing is written under .wrangler/), replays the schema of the database the type check reads into it, and answers EXPLAIN <statement> requests over IPC, binding one null per parameter.
  • src/sqlite-drivers.ts adds it as one more driver beside those of the previous change, and each driver now words its own refusal.
  • The child starts in about half a second, on the first statement that needs it; each statement then takes a few milliseconds. The IPC channel holds the parent's event loop only while a statement waits, and the child exits when the parent goes; a type check of a scratch project left no workerd process behind.
  • A project with no D1 binding, or one whose workerd does not start within 30 seconds, is not checked.

Checks

  • tests/sqlite-drivers.test.ts: D1's refusals of json_pretty and ceiling with their message and position; ceil, an insert … returning and a two-argument iif with their parameters bound and no error; a project whose config binds no D1 database.
  • lab/drivers gains wrangler, which the tests load; CI installs it.
  • examples/cloudflare-workers, whose config binds D1, type-checks, tests, serves, builds and dry-runs a deploy with the check on.
  • The SQLite reference page and the example's README describe it.

D1 refuses functions its SQLite has, such as json_pretty and power, through an allowlist nothing lists, so a statement is compiled with EXPLAIN on D1 as the project's wrangler runs it locally.
Copilot AI balanced review requested due to automatic review settings October 22, 2026 22:13
@vercel
vercel Bot commented Oct 22, 2026 •
Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
reylo Ready Ready Preview Oct 22, 2026 10:33pm UTC

Request Review

Copilot AI left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

With a D1 binding marked "remote": true, this PR makes the type check run the local schema's DDL on the real remote D1 database. The schema D1 sees is also a snapshot that never refreshes in the editor, so after a migration it reports false errors.

Reviewed changes

I reviewed the D1 check end to end: the proxy child, how the driver opens and its IPC, the refusal wording and placement, the tests and the docs.

  • src/d1-proxy.ts: a forked child calls wrangler's getPlatformProxy({ persist: false }), replays the schema's DDL into the first D1 binding and answers explain <sql> requests over IPC, binding one null per parameter.
  • openD1 in src/sqlite-drivers.ts: reads the DDL through node:sqlite, forks the proxy and waits up to 30s for it to start. The IPC channel holds the event loop only while a request is pending.
  • SqliteDriver interface: version is replaced by says(refusal), so each driver words its own message. refuses now takes the binding count (rewritten.order.length).
  • placeRefusal: also places D1's not authorized to use function: x at the call.
  • Tests and docs: wrangler joins lab/drivers. New tests cover D1's refusals of json_pretty and ceiling, statements it allows, and a config with no D1 binding. The SQLite reference page and the example README describe the check.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread src/d1-proxy.ts Resolved
Comment thread src/d1-proxy.ts Resolved
Comment thread src/sqlite-drivers.ts Resolved
Comment thread src/sqlite-drivers.ts Resolved
A binding marked remote is never proxied to the real database, D1 gets the schema again whenever it changes, and workerd starts once per project rather than with each connection.
@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ All four earlier findings are fixed. One minor suggestion inline, about the failure path of the new per-project cache.

Reviewed changes

I reviewed facbc5f, which keeps D1 local, keeps its schema current, and runs one child per project.

  • Local only: getPlatformProxy now gets remoteBindings: false, so a binding marked remote never reaches the production database.
  • Schema kept current: refuses reads the DDL again for each statement, and the child gets a new schema only when the DDL differs from the last one sent. replay() drops the earlier views and tables before it runs the new DDL. The child handles one message at a time, so a new schema is in place before the statements sent after it compile.
  • One child per project: the new d1s map, keyed by wrangler entry and project directory, holds the D1 child for the life of the process. Closing a connection now closes only its read handle, and ended() removes a child from the map when it exits.
  • Test: a session adds a column and a table partway through, then calls recheck() and expects no diagnostic. A later session reuses the child.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread src/sqlite-drivers.ts Resolved
@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. The earlier finding about failed D1 starts is fixed, and I resolved its thread.

Reviewed changes

I reviewed 78d1a69, which remembers a failed D1 start until the wrangler config changes.

  • Failed starts stay cached: a d1s entry is now { config, started }, and a rejected started stays in the map. A project that binds no D1 database, or whose workerd times out, starts workerd once per process instead of once per connection.
  • Config-keyed invalidation: wranglerConfig(dir) records the name, size and mtime of wrangler.json, wrangler.jsonc and wrangler.toml. When the record no longer matches, the old child is closed through the new D1Process.close and a new one starts.
  • Eviction only after a successful start: a running flag means ended() runs only for a child that exits after it started. The d1s.get(key)?.started === started guard stops a closed child's exit from evicting the child that replaced it.
  • Test: the no-binding test now runs two sessions, then rewrites the config with a D1 binding and expects the json_pretty refusal.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@colinhacks
colinhacks merged commit 49ce965 into main Oct 22, 2026
6 checks passed
@colinhacks
colinhacks deleted the d1-statement-check branch October 22, 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.

2 participants

0