Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/sqlfu/docs/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ const rows = await stmt.all({slug: 'hello-world'});

Prepared handles expose `.all(params)`, `.run(params)`, and `.iterate(params)`. `params` can be a positional array (`[id]`) or a named object (`{slug}`). Adapters that have native prepared statements hold the driver handle and dispose it when the `using` scope exits. Adapters whose driver only exposes an `exec`/`execute` API provide a compatible shim: the method still exists, but each call re-issues the SQL through the driver.

Named object keys are bare identifiers. SQL written as `:slug`, `@slug`, or
`$slug` all bind from `{slug: 'hello-world'}`; sqlfu adapters translate that
shape to the underlying driver, including sqlite-wasm. For positional `?`
placeholders, pass an array.

## Compatibility matrix

Expand Down
58 changes: 44 additions & 14 deletions packages/sqlfu/docs/typegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,34 @@ statement in that file must have its own `@name`.

## Parameter forms

Plain params use sqlfu's normal `:name` placeholder syntax.
Generated query wrappers use sqlfu's `:name` placeholder syntax. The placeholder
name becomes the application-facing param name exactly as you wrote it, so write
`:publishedSince` if you want callers to pass `{publishedSince}`.
`generate.casing` does not rewrite placeholder names.

Use the SQL shape to describe richer params:

- `where id = :id`: one scalar value.
- `where id in (:ids)`: one non-empty scalar array.
- `values (:post.slug, :post.published_at)`: one object named `post`; its
generated member fields follow `generate.casing`.
- `insert into posts (slug, title) values :posts`: one object or a non-empty
object array whose generated member fields come from the INSERT column list.
- `where (slug, published_at) in (:keys)`: a non-empty object array whose
generated member fields come from the row-value tuple.

Top-level placeholder names are preserved literally: `:post`, `:posts`, and
`:keys` become `post`, `posts`, and `keys`. Object member fields are generated
SQL-derived fields, so the default `generate.casing: 'camel'` boundary applies
to them. Use `generate.casing: 'preserve'` if callers should pass raw SQL names
such as `published_at`.

Runtime-expanded forms (`IN (:ids)`, row-value `IN (:keys)`, and
`values :posts`) rewrite the SQL before execution so the generated wrapper can
bind the right number of driver args for each call. That is why they reject
empty arrays and why each expanded param can appear only once in a query.

Plain params are the default form.

```sql
/** @name getPost */
Expand Down Expand Up @@ -164,43 +191,46 @@ Use dot paths when a query naturally accepts one object.

```sql
/** @name insertPost */
insert into posts (slug, title)
values (:post.slug, :post.title)
returning id, slug, title;
insert into posts (slug, published_at)
values (:post.slug, :post.published_at)
returning id, slug, published_at;
```

```ts
await insertPost(client, {
post: {
slug: 'hello-world',
title: 'Hello world',
publishedAt: '2026-05-14',
},
});
```

The generated params type is `{post: {slug: string; title: string}}`. One object
path segment is supported today; nested paths such as `:post.author.id` are
intentionally rejected until the type shape is designed.
The generated params type is `{post: {slug: string; publishedAt: string}}`
under the default camel casing. One object path segment is supported today;
nested paths such as `:post.author.id` are intentionally rejected until the type
shape is designed.

Use an object param directly after `values` when an INSERT column list already
names the object fields. The generated param accepts either one object or a
non-empty array.
names the object fields. The placeholder name is still preserved, but the object
fields are column-derived, so the default `generate.casing: 'camel'` boundary
applies to them. The generated param accepts either one object or a non-empty
array.

```sql
/** @name insertPosts */
insert into posts (slug, title)
insert into posts (slug, published_at)
values :posts;
```

```ts
await insertPosts(client, {
posts: {slug: 'first', title: 'First'},
posts: {slug: 'first', publishedAt: '2026-05-14'},
});

await insertPosts(client, {
posts: [
{slug: 'second', title: 'Second'},
{slug: 'third', title: 'Third'},
{slug: 'second', publishedAt: '2026-05-14'},
{slug: 'third', publishedAt: '2026-05-15'},
],
});
```
Expand Down
7 changes: 4 additions & 3 deletions packages/sqlfu/src/sqlite-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ export function sqlReturnsRows(sql: string): boolean {
* tokenizer skips string literals and comments so colons/dollars inside
* quoted strings aren't mistaken for placeholders.
*
* Adapters whose driver natively accepts `Record` bindings (better-sqlite3,
* node:sqlite, libsql, sqlite-wasm) skip this helper and pass the params
* straight through.
* Adapters whose driver natively accepts bare-key `Record` bindings
* (better-sqlite3, node:sqlite, libsql) skip this helper and pass the params
* straight through. sqlite-wasm accepts prefixed keys, so its adapter maps
* sqlfu's bare keys to the SQL placeholder prefix before binding.
*/
export function rewriteNamedParamsToPositional(
sql: string,
Expand Down
27 changes: 27 additions & 0 deletions tasks/improve-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ Go through all of the docs and improve them. Look for inconsistencies, informati

I want you to think holistically about the docs, as mentioned in your agent instructions for this repo. From time to time that will mean re-thinking docs entirely. Sometimes it will mean creating a separate task in the branch you create for *changing the product* instead. Docs are most useful when not thought of as something entirely downstream from the product code - if there's no clear way to write about a concept, the concept might be poorly thought through. You are free to flag that as an alternative to documenting weirdness.

## 2026-05-14 pass

Status: done for this bedtime pass. The first decision was to keep the pass small and focused on generated query docs after recent merges: #113 extracted parameter expansion internals, #108 clarified the generated casing boundary, and #101 fixed sqlite-wasm named-parameter binding. The docs now clarify generated wrapper parameter forms in `packages/sqlfu/docs/typegen.md` and prepared-statement named binding in `packages/sqlfu/docs/adapters.md`; checks are green for the focused docs path and PR #115 has the reviewer-facing summary.

Scope:

- Tighten docs users read when choosing parameter shapes for generated query wrappers.
- Keep `packages/sqlfu/README.md` mostly stable unless a tiny overview sentence is needed.
- Prefer `packages/sqlfu/docs/typegen.md` and, if useful, the prepared statement / sqlite-wasm docs over landing-page or website information architecture work.
- Do not move this evergreen task to `tasks/complete/`.

Checklist:

- [x] Create an isolated worktree from `origin/main` and commit this status note first. _worktree is `/Users/mmkal/src/worktrees/sqlfu/bedtime-2026-05-14-improve-docs` on branch `bedtime/2026-05-14-improve-docs`._
- [x] Open the early PR after the task-only commit. _PR #115 opened after `2afe32a` so the scope note is reviewable before implementation._
- [x] Improve generated query parameter docs based on the recent merged parameter and casing work. _`typegen.md` now separates authored placeholder names from column-derived object fields and calls out runtime-expanded param limits; `adapters.md` records bare-key binding for `:name` / `@name` / `$name` prepared statements._
- [x] Run relevant docs/package checks. _After `pnpm install`, `pnpm --dir packages/sqlfu exec vitest run test/generate/query-parameters.test.ts`, `pnpm --filter @sqlfu/ui build`, and `pnpm --filter sqlfu-website build` passed. An initial mis-scoped `pnpm --filter sqlfu test:node -- test/generate/query-parameters.test.ts` ran the full sqlfu suite and hit existing non-doc failures in `resolve-sqlfu-ui` / strict imports._
- [x] Update the PR body with the net reviewer/user effect. _PR #115 now explains the docs change from the reader/reviewer perspective and lists the checks run._

Implementation notes:

- 2026-05-14: Read the repo instructions and `writing-well` checklist before editing. The main checkout has unrelated website edits, so this pass is staying entirely in the requested worktree.
- 2026-05-14: Recent merge scan points at `packages/sqlfu/docs/typegen.md` as the primary docs surface. The current page has the right facts, but the parameter section can better separate authored placeholder names, column-derived object fields, runtime placeholder expansion, and sqlite-wasm/prepared-statement named binding.
- 2026-05-14: Chose not to churn the README. It already has a compact parameter summary; the detailed mental model belongs in the Type generation page and the prepared-statement note belongs with adapters.
- 2026-05-14: Em-dash scan found existing occurrences in historical task logs, agent docs, code comments, and UI test labels. The touched source docs are clean for new em-dashes; the only matches in `tasks/improve-docs.md` are older pass notes.
- 2026-05-14: Updated PR #115 body after pushing the docs commit. No follow-up task was split out because the broader README already points at the deep-dive page and did not need another local summary.

## 2026-05-09 pass

Status: done for this bedtime pass. The main docs work was the aggressive rewrite of `website/src/content/blog/introducing-sqlfu.md`: it dropped from about 2,750 words to about 1,250 while preserving the SQL-first argument, iterate/agent motivation, example query, feature inventory, limits, and acknowledgements. A small em-dash sweep also cleaned the newly-added Cloudflare D1 docs/README prose. Website build is green.
Expand Down
Loading