Repository navigation
feat(engine)!: replace the coalesce rule with nullable(…) casts - #79
Conversation
A parameter cast to nullable(type) takes null and is the type inside otherwise; the cast is written alone in the compiled statement. A coalesce of a parameter no longer makes it nullable. A marked parameter written to a column that refuses null is an error.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
ℹ️ 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.rsreads 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, MySQLmysql_params.rsand SQLitecoalesced_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.tsand the docs capture now make every nullable parameterq?: T | null. The run time already binds a missing key as NULL, andcheckCastskips null. - Docs and skill: Nullable parameters and each dialect's rules are rewritten. Dynamic filtering is now taught as one typed JSON
filtersparameter 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.
claude-opus-5-5 | 𝕏
There was a problem hiding this comment.
✅ 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 newAnalysis::selectedlist now holds every parameter, bare or cast, that is a whole column of the SELECT feeding an INSERT. Onlynullable_paramsreads that list, sofilledand the typing path stay as they were. On MySQL,fillednow takes aFillmode (Typed/Bare/Written), andnullableandwrittenboth readWritten. Each engine has a test, SQLite included. - One type inside a Postgres
nullable(…): The newone_postgres_typecheck 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.tsnow says a parameter takes null when it is cast tonullable(…)or written bare to a nullable column. - Artifacts: All three
engine.wasmfiles were rebuilt.
claude-opus-5-5 | 𝕏

A parameter that takes null is now written
:p::nullable(type), andcoalesceno longer makes a parameter nullable.Why
coalescestood in for a type annotation. Measured on Postgres 18 before this change:where created_at > coalesce(:since::timestamptz, created_at)string | nullx > xis false)with input as (select :since::timestamptz as since) … where input.since is null or p.created_at > input.sincestring, refusing nullcoalesce(:since::timestamptz, null)in thewithstring | nullcoalescewritten only to change the typeA cast states a parameter's whole type, so the cast is where "or null" belongs.
What changes
::nullable(type)makes the parameter that type ornull, 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:The coalesce rule is gone on all three dialects, with the engines' tracking of a parameter through a
withcolumn or a derived table. Acoalesceor a null test of a parameter alone types nothing.A parameter that takes null may be left out, which binds NULL:
q?: T | nullinParams, and absent fromrequiredin 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 (itsNOT 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.rsreads::nullable(…)(Postgres keeps the inner type as written), writes the inner cast, and hands each dialect the marked names (innertakes them).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.tsandsrc/schema.tsmake 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.guard_withstatements now take null;mysql_nullable_params/guard_nullable.sqlis new.enumand default), each dialect page's rules, the skill,lab/pg-forms,lab/mysql,lab/sqlite, and the rebuiltengine.wasmfor all three databases.