Skip to content

feat(query): a pipeable, data-last Query API next to the fluent builder - #102

Merged
Makisuo merged 6 commits into
MapleTechLabs:mainfrom
artile:feat/pipe-api
Nov 2, 2026
Merged

Makisuo merged 6 commits into
MapleTechLabs:mainfrom
artile:feat/pipe-api

Conversation

@artile

@artile artile commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

A query has only fluent methods. In an Effect codebase you cannot compose reusable query parts with pipe, as you do with Effect, Stream and Schema.

Closes #65.
Part of #52.

Changes

  • CHQuery is now Pipeable (effect Pipeable): query.pipe(f, g) is g(f(query)).
  • A new Query namespace on /clickhouse and /postgres (from src/core.ts, file src/ch/query-pipe.ts): select, where, orderBy, limit, innerJoin, leftJoin, crossJoin, plus from, fromQuery and fromUnion as start points.
  • Each function is a Function.dual that calls the fluent method, so the two styles share one implementation. Each function has a data-last form (query.pipe(Query.where(fn))) and a data-first form (Query.where(query, fn)).
  • $ in a data-last callback gets its type from the query that pipe carries. A wrong column, an orderBy key that is not selected, a negative limit and an alias that is already in use are type errors at that step.
  • Tests: src/ch/query-pipe.test.ts compares the compiled SQL of the data-last, data-first and fluent forms for each function and for a full chain (ClickHouse and Postgres SQL, parameters, tenant scope). src/ch/query-pipe.test-d.ts checks that the result types are the same as the fluent types, that params propagate, and the type errors above. A new core case pipe-data-last runs a piped chain live on ClickHouse (both join modes) and on Postgres.
  • Docs: a new "Pipeable, data-last queries" section in docs/queries.md (backed by a test in docs-examples.test.ts), a Query table in docs/reference.md, and one changeset (minor).

Left out

  • Data-last forms of the other CHQuery methods: groupBy, having, offset, format, distinct, distinctOn, the row locks, route, crossTenant, innerJoinQuery / leftJoinQuery / crossJoinQuery and withCTE. They follow the same pattern. I kept this PR small, so that you can review the shape first. The issue's "done when" asks for every method. I can add the rest in this PR or in a follow-up.
  • Pipeable and data-last forms for CHUnionQuery, insertInto, update and deleteFrom.
  • effect-qb's order, Query.select(...).pipe(Query.from(t)). Here a query starts at from, because the column types come from the table. A select before from would need a different builder state.
  • Helpers that are generic over many tables (for example byTenant for every table with an OrgId column). Comparison operands on a generic column type do not resolve, in the fluent builder too. A reusable part for one table works: type its $ with ColumnAccessor<typeof Table.columns> (see the docs).
  • A data-last function uses the exact column type from pipe, as Effect.map does. If you type a callback's $ with fewer columns than the query has, the query type narrows to those columns.

Validation

All commands ran on this branch, rebased on upstream/main (c5fa679), with a live ClickHouse 26.8.2.7 (EFFECT_ORM_CLICKHOUSE_URL / _USER / _PASSWORD set, so no suite skipped):

  • bun run build: ok.
  • bun run typecheck: exit 0.
  • bunx vitest run --testTimeout=60000 --hookTimeout=60000: 61 files passed, 1046 tests passed, 0 skipped.
  • node scripts/check-doc-citations.mjs: ok (25 files).
  • node scripts/check-exports-documented.mjs: ok (288 /clickhouse, 206 /postgres exports).
  • node scripts/check-doc-examples.mjs: ok (21 files typechecked).
  • bun scripts/check-package.ts: tarball imports, types, compilation and codecs passed; CommonJS consumer ok.

Upstream CI for a fork PR waits until a maintainer approves the run.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 42 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 28e2c9de-223c-427f-bbb8-116c5e33278b
Commits

Reviewing files that changed from the base of the PR and between b57faf1 and cd185b4.

Files ignored due to path filters: 1
  • tests/__snapshots__/core-sql.test.ts.snap is excluded by !**/*.snap
Files selected for processing: 13
  • .changeset/pipeable-query-api.md
  • README.md
  • docs/README.md
  • docs/queries.md
  • docs/reference.md
  • src/ch/query-pipe.test-d.ts
  • src/ch/query-pipe.test.ts
  • src/ch/query-pipe.ts
  • src/ch/query.ts
  • src/core.ts
  • src/docs-examples.test.ts
  • tests/core-cases.ts
  • tests/dialect-coverage.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Share

Makisuo and others added 2 commits November 2, 2026 16:32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds Query.groupBy, having, offset, format, distinct, distinctOn, the four
row locks, route, crossTenant, innerJoinQuery / leftJoinQuery /
crossJoinQuery and withCTE, as the issue asks for every builder method. The
subquery joins take a query first, so they tell data-first from data-last
by arity. A test checks that every fluent method has a Query function, and
that piped, data-first and fluent forms compile to the same SQL (or the same
refusal) on ClickHouse, Postgres, SQLite and D1. Records the sqlite and d1
snapshots of the live pipe-data-last case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Makisuo

Makisuo commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Merge-readiness pass (maintainer side):

  • Merged current main. No conflicts. Query is now on /sqlite too, through core.ts.
  • Done that the issue asks for: every CHQuery method now has a data-last and a data-first form. Added groupBy, having, offset, format, distinct, distinctOn, forUpdate / forNoKeyUpdate / forShare / forKeyShare, route, crossTenant, innerJoinQuery / leftJoinQuery / crossJoinQuery and withCTE. The subquery joins take a query as their first data-last argument, so isQuery(args[0]) cannot tell the two forms apart there. They use arity instead. withCTE starts data-last with a string name, so isQuery still works for it.
  • Tests (src/ch/query-pipe.test.ts):
    • A guard that every key on a runtime CHQuery has a Query function.
    • For each new function, the piped, data-first and fluent forms give the same outcome on ClickHouse, Postgres, SQLite and D1. That is the same SQL, params and tenant scope, or the same refusal where the dialect lacks the form: row locks on ClickHouse/SQLite/D1, format outside ClickHouse, DISTINCT ON on SQLite/D1.
    • Type tests (query-pipe.test-d.ts) for the new functions: the type equals the fluent type, unselected groupBy / distinctOn keys and a negative offset are errors, and a subquery join's params propagate.
  • The live pipe-data-last core case now also runs on SQLite and D1 (snapshots recorded).
  • Docs and changeset updated to the full surface. Still left out, and documented as such: CHUnionQuery and the writes (insertInto / update / deleteFrom) are not Pipeable.

Verified locally against ClickHouse 26.8: bun run build, bun run typecheck, bun run test (74 files / 1453 tests, plus the doc and export checks), live tests/*.clickhouse.test.ts plus core.sqlite / core.postgres (11 files / 290 tests). vitest run tests/fuzz with FUZZ_SCALE=0.3 gave 1 flaky failure in 2 of 8 runs of queries.fuzz. The one I captured was the known ClickHouse Division by zero on a generated fixture, which #131 fixes. The fuzz suite does not use Query.

@Makisuo
Makisuo merged commit c4f0f39 into MapleTechLabs:main Nov 2, 2026
2 of 3 checks passed
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.

A pipeable, data-last query API next to the fluent builder

2 participants