8000 feat(engine)!: replace the coalesce rule with nullable(…) casts by colinhacks · Pull Request #79 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

feat(engine)!: replace the coalesce rule with nullable(…) casts - #79

Merged
colinhacks merged 11 commits into
mainfrom
nullable-cast
Oct 23, 2026
Merged

colinhacks merged 11 commits into
mainfrom
nullable-cast

Conversation

@colinhacks
@colinhacks colinhacks commented Oct 23, 2026 •
Copy link
Copy Markdown
Owner

A parameter that takes null is now written :p::nullable(type), and coalesce no longer makes a parameter nullable.

Why

coalesce stood in for a type annotation. Measured on Postgres 18 before this change:

Statement Typed What null does
where created_at > coalesce(:since::timestamptz, created_at) string | null matches no row (x > x is false)
with input as (select :since::timestamptz as since) … where input.since is null or p.created_at > input.since string, refusing null the query that handles null correctly
the same with coalesce(:since::timestamptz, null) in the with string | null a no-op coalesce written only to change the type

A cast states a parameter's whole type, so the cast is where "or null" belongs.

What changes

  • ::nullable(type) makes the parameter that type or null, for any type the dialect casts to (timestamp with time zone, int[], varchar(255), a schema's type) and for a typed JSON parameter (::nullable(jsonb('{…}'))). The compiled statement holds the cast inside alone, since a cast of NULL is NULL in every dialect:

    select id, title from posts where author_id = coalesce(:author_id::nullable(int), author_id)
    -- Postgres: … coalesce($1::int, author_id)
    -- MySQL:    … coalesce(CAST(? AS SIGNED), author_id)
    -- SQLite:   … coalesce(CAST(? AS INTEGER), author_id)
    -- Params:   { author_id?: number | null }
  • The coalesce rule is gone on all three dialects, with the engines' tracking of a parameter through a with column or a derived table. A coalesce or a null test of a parameter alone types nothing.

  • A parameter that takes null may be left out, which binds NULL: q?: T | null in Params, and absent from required in the input schema, the bare write to a nullable column included. With the run-time check of fix: refuse null for a parameter the statement takes no null for #75 (.run() refuses null for every other parameter), the marker is the declaration .run() enforces.

  • A nullable(…) parameter written whole to a column that refuses null (its NOT NULL, a CHECK, a domain) is a type error at the parameter.

  • Kept: a bare parameter written whole to a nullable column takes null without the marker.

Where

  • engine/core/src/structured.rs reads ::nullable(…) (Postgres keeps the inner type as written), writes the inner cast, and hands each dialect the marked names (inner takes them).
  • Postgres (postgres.rs, analyze.rs), MySQL (mysql.rs, mysql_params.rs) and SQLite (sqlite.rs) take the marks in place of their coalesce rules and report a marked parameter written to a column refusing null.
  • src/module-types.ts and src/schema.ts make a nullable parameter optional; the docs' capture renders it the same way, and its JSON-cast rewrite no longer swallows a call's own parenthesis.
  • Oracle: the statements whose point was a parameter taking null use the cast (their types unchanged); the rest are re-blessed with the coalesced parameters as values; the guard_with statements now take null; mysql_nullable_params/guard_nullable.sql is new.
  • Docs: Inference's Nullable parameters, and Dynamic filtering taught as one typed JSON parameter (its schema gives each filter its type, enum and default), each dialect page's rules, the skill, lab/pg-forms, lab/mysql, lab/sqlite, and the rebuilt engine.wasm for all three databases.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 18:56
@vercel
vercel Bot commented Oct 23, 2026 •
Copy link
Copy Markdown

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

1 Skipped Deployment 8000
Project Deployment Actions Updated
reylo Ignored Ignored Preview Oct 23, 2026 7:14pm 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 insufficient remaining quota.

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

Choose a reason for hiding this comment

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

ℹ️ No critical issues. Two gaps in the new nullable(…) checks, details inline.

Reviewed changes

I reviewed the whole PR at a902126: the ::nullable(…) cast, the removal of the coalesce rule on every dialect, the refused-null diagnostic, the optional-parameter typing, and the docs, labs and oracle updates.

  • ::nullable(type) cast: structured.rs reads the cast and writes the inner cast alone. It then passes the marked names to each dialect's analysis, which takes them in place of its own rule.
  • Coalesce rule removed: The coalesce/CTE/derived-table tracing is gone from Postgres analyze.rs, MySQL mysql_params.rs and SQLite coalesced_params. Oracle cases now use the cast, and the rest are re-blessed with values.
  • Refused-null diagnostic: A marked parameter written whole to a column that refuses null (its NOT NULL, a CHECK or a domain) is now an error at the parameter, on all three engines.
  • Optional nullable parameters: module-types.ts, schema.ts and the docs capture now make every nullable parameter q?: T | null. The run time already binds a missing key as NULL, and checkCast skips null.
  • Docs and skill: Nullable parameters and each dialect's rules are rewritten. Dynamic filtering is now taught as one typed JSON filters parameter per dialect.

ℹ️ Stale comment on when a parameter takes null

A comment at tests/render.test.ts:222 still says a top-level parameter "takes NULL only as the first argument of coalesce". After this PR, that is no longer true. It could say nullable(…) instead.

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

Comment thread engine/postgres/src/postgres.rs Resolved
Comment thread engine/core/src/structured.rs Resolved
@colinhacks
Copy link
Copy Markdown
Owner Author

On the review's stale comment at tests/render.test.ts:222: reworded in 2ce0e82. A top-level parameter now takes NULL only when cast to nullable(…) or written bare to a nullable column. The two inline points are answered in their threads; the three artifacts were rebuilt in 3cdbbca.

@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 new commits fix all three points from the last review.

Reviewed changes

I reviewed the four commits since a902126, which close the gaps in the nullable(…) checks and rebuild the engine artifacts.

  • Refused-null check through INSERT … SELECT: On Postgres, a new Analysis::selected list now holds every parameter, bare or cast, that is a whole column of the SELECT feeding an INSERT. Only nullable_params reads that list, so filled and the typing path stay as they were. On MySQL, filled now takes a Fill mode (Typed / Bare / Written), and nullable and written both read Written. Each engine has a test, SQLite included.
  • One type inside a Postgres nullable(…): The new one_postgres_type check accepts a name (qualified or quoted), its modifiers, [digits] array bounds, and the extra words of Postgres's own multi-word types. It refuses commas, operators, strings and any other word with the same "one type alone" message MySQL and SQLite give. Tests cover both the accepted and the refused cases.
  • Stale comment: The comment in tests/render.test.ts now says a parameter takes null when it is cast to nullable(…) or written bare to a nullable column.
  • Artifacts: All three engine.wasm files were rebuilt.

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

@colinhacks
colinhacks merged commit 57307bf into main Oct 23, 2026
6 checks passed
@colinhacks
colinhacks deleted the nullable-cast branch October 23, 2026 19:21
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