Repository navigation
Refuse invalid queries before SQL is sent: typed params, GROUP BY and dialect checks - #18
Merged
Merged
Conversation
- A second where()/having() ANDs with the first instead of replacing it,
so a shared base keeps its tenant filter.
- Comparisons refuse null (type error, and a QueryBuilderError at runtime);
an empty IN list renders as 1 = 0 / 1 = 1 instead of IN ().
- like/notLike/ilike accept a nullable string column.
- limit/offset reject negative or fractional literals, and NaN, negative or
fractional runtime values, instead of emitting or rounding them.
- Join aliases must not repeat or shadow a FROM column (type error), nor
repeat the FROM alias; CTE names must be unique (compile-time defect).
- A query with no select() cannot be run, compiled, joined, used in FROM,
a CTE, EXISTS or INSERT ... SELECT.
- unionAll branches must agree on aliases and column types; an empty union
is a type error.
- inSubquery/notInSubquery require exactly one column of a comparable type.
- update().set({}) is a type error; an UPDATE or DELETE without where() or
allRows() cannot be compiled or run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er dialect Each clause is rendered under a render track that records the columns it reads outside an aggregate and whether it aggregates. compile uses it to refuse an aggregate in WHERE or a join's ON, a selected or HAVING column that is neither grouped nor aggregated, and grouping by an aggregate. Only SQL the builder writes is counted; raw SQL, templates, windows and caller-declared functions are opaque, so they can hide an error but never cause a false one. Built-in functions now belong to a function set. A ClickHouse function in a Postgres compile (count() instead of count(*)) is a defect, and the reverse; coalesce, nullIf and lower stay portable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expr and Condition carry the params inside them (a contravariant phantom, so an Expr<T, P> still goes wherever an Expr<T> does). Queries, unions, inserts, updates and deletes collect them from every clause, subquery, join and CTE; compile, compileUnion and Database.run require each one with a value of its type. Built-in functions, defineFn/defineCondFn, subquery predicates and CH.sql pass their arguments' params on. Insert rows and SET records also reject columns the table cannot write, now that their types are inferred. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing (48)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- makeExpr / makeUntypedExpr / makeCond take the expressions they interpolate as `uses` and carry their params; compiling one whose SQL holds a param no `uses` entry carries is a defect, so a param can no longer reach a query without being in its type. - An explicit type argument on makeExpr, subqueryExpr or compileTypedFnCall is now an error (the inferred parameter comes first) instead of silently dropping params; untypedSubqueryExpr returns Expr<unknown>. - inSubquery / notInSubquery check at compile time that the subquery selects exactly one column, for callers past the types. - param.dateTimeString / dateTimeSeconds accept a Date or DateTime.Utc in the type, as they do at runtime. - Integration fixtures and the docs behaviour check no longer rely on a second where() replacing the first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tarball check compiles a query without its param to exercise the runtime failure; params are now in the type, so that call is a type error too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Compared the builder's type safety with Kysely (0.29.6) by writing ~45 deliberately invalid queries and checking each against
tsc,compile, and PGlite. Many type-checked and either emitted invalid SQL or SQL that cannot mean what it says. This PR closes those holes. The goal it enforces: every invalid query is either a type error or a typedQueryBuilderError/QueryBuilderDefectfromcompile, before any SQL is sent.Three commits, one per tier:
1. Refuse invalid queries the builder used to accept
where()/having()ANDs with the first instead of replacing it (as Kysely does), on queries and writes, so a shared base keeps its tenant filter.null(type error, plus aQueryBuilderErrorat runtime); useisNull(). Emptyin_()/notIn()render as1 = 0/1 = 1instead ofIN ().like/ilikeaccept nullable strings (previously rejected).limit/offsetreject negative or fractional literals at the type level andNaN/ negative / fractional runtime values at compile, instead of emittingLIMIT NaNor rounding.TypeError).select()cannot be compiled, run, joined, used inFROM, a CTE,EXISTS, orINSERT ... SELECT.unionAllbranches must agree on aliases and compatible column types; an empty union is a type error.inSubquery/notInSubqueryneed exactly one column of a comparable type.update().set({})is a type error; an UPDATE / DELETE withoutwhere()orallRows()cannot be compiled or run (phantom ready-state).2. Aggregates, GROUP BY, and dialect function sets
src/sql/render-tracker.ts) that records columns read outside an aggregate and whether the clause aggregates.compilerefuses an aggregate in WHERE or a join's ON, a selected/HAVING column that is neither grouped nor aggregated, and grouping by an aggregate.CH.sqltemplates, windows, and caller-declared functions are opaque, so they can hide an error but never cause a false one.Dialect.functions):CH.count()(which emitscount()) in a Postgres compile is a defect, and the reverse.coalesce,nullIf, andlowerstay portable.3. Params in the type
Expr<T, P>/Condition<P>carry theparam.*placeholders inside them, via a contravariant phantom, soExpr<T, P>still goes whereverExpr<T>does.compile,compileUnion, andDatabase.runrequire each param with a value of its type, and the error spells outparamsRequired: { … }. Extra keys are allowed.defineFn/defineCondFn, subquery predicates, window specs, andCH.sqlpass their arguments' params on.Reviewer notes
CHANGELOG.mdunder Unreleased;docs/queries.md,expressions.md,updates-and-deletes.md,tenant-scoping.md,postgres.md,params-and-compilation.md, andextending.mdare updated.compile.test.ts, an ungrouped column next toGROUP BY). Code passing params as an untypedRecord<string, unknown>will need a typed object.makeExprdrops param tracking unless its signature carriesQ(shown indocs/extending.md).untypedSubqueryExpr<T>(…)with an explicit type argument does the same. In both casescompilestill checks the params at runtime. TheIN (subquery)column-count check is type-level only.where) are now also marked@ts-expect-error, so they cover both layers.src/ch/soundness.test-d.ts,src/ch/soundness.test.ts,src/ch/params-propagation.test-d.ts.Testing
tsc --noEmit(both configs): cleanvitest run: 584 passed, 193 skipped (integration suites needing a live ClickHouse/Postgres; not run here)tsdownbuild: pass🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by CodeRabbit
where()andhaving()calls now combine conditions with AND; emptyINlists compile to predictable true/false results.likeandilike.LIMITandOFFSETvalues and comparisons againstnullorundefinednow produce clear errors rather than generating invalid SQL.