Skip to content

feat: composable typed queries, aggregates and composite cursors - #229

Merged
andrewzolotukhin merged 3 commits into
developmentfrom
feat/composable-typed-queries
Sep 21, 2026
Merged

andrewzolotukhin merged 3 commits into
developmentfrom
feat/composable-typed-queries

Conversation

@andrewzolotukhin

@andrewzolotukhin andrewzolotukhin commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Original request

Implement point 1 (1.1–1.4: composable typed database queries) from the consumer-experience proposal as a separate Framework PR against development. Keep the APIs application-agnostic, preserve existing consumers, add documentation/tests, and use minor changesets only. Include every aggregate family and make aggregate output schemas optional.

What changed

  • Flat typed joins/projections: immutable alias(schema, name), typed multi-table selectors, eq/and/or predicates, inner/left joins, and inferred flat results (including left-join nullability). Each aliased source retains its scopes and soft-delete filters. Supports bound queries, transactions, grouped projections and SQL-native HAVING.
  • Reliable eager loading: carry parent ordering through the outer query, including mapped projections, bound raw order expressions, projected aliases/positions, limits/offsets and distinct/grouped parents. Strip internal ordering columns from mapped results. Infer related-query customization types in ORM includes.
  • All aggregate families: additive scalar countValue/countDistinctValue/sumValue/avgValue/minValue/maxValue helpers and matching aggregate.* expressions for grouped projections. Counts are checked safe integers; sum/avg and exact numeric extrema preserve database text. Optional output schemas replace decoding and infer their output types. Scalar helpers retain filters/transactions while ignoring pagination without mutating their source. Aggregate DTOs are not attached to the ORM identity map.
  • Composite cursors: opt-in paginateAfter({ orderBy, limit, cursor }) supports mixed directions and schema-declared unique tie-breakers. Versioned cursors retain exact database timestamp/numeric values. Required eager relations filter before page limits. Unsupported query shapes and malformed/incompatible cursors fail explicitly; old single-column cursor calls remain unchanged.
  • Verification: runtime/type regressions plus 25 real PostgreSQL integration cases through built public package APIs. Added a dedicated PostgreSQL 16 CI job; missing test database configuration is a hard failure.
  • Documentation/release: package guide, README examples, website API documentation, and minor changesets for @cleverbrush/knex-schema and @cleverbrush/orm.

No Xpenser code, dependency versions, production deployment, or local consumer-experience document changes are included.

Review follow-up

Addressed all four inline comments and the package-wide JSDoc request in dbca5c28:

  • Keep the query() implementation generic over the source schema and alias, with an explicit typed return. Give createQuery() real overloads and use satisfies BoundQuery instead of casting the bound factory. Type regressions cover ordinary/aliased queries, base queries, transactions, inferred projections, nullable joins, invalid fields, and ORM re-exports.
  • Export isSqlIdentifier(value) and reuse it for alias validation. Document its conservative ASCII single-identifier contract; test valid names, malformed strings, and non-string inputs.
  • Demonstrate recursive and(...or(...and(...))) predicates in consumer docs and SQL tests, and assert their actual PostgreSQL results. The existing predicate engine already supports this syntax, so no redundant runtime rewrite was needed.
  • Replace all inline import('@cleverbrush/schema').InferType uses in dbset.ts with one top-level type import.
  • Document public functions, class members, overloads, factory variables, aggregate methods and relevant options across both query/ORM packages, including existing APIs. Test JSDoc in emitted declarations to protect installed-consumer IDE hints; private/internal and third-party APIs are excluded.

Changeset bumps remain minor. The local consumer-experience proposal is untouched and is not committed.

Reasoning

These additions remove repeated consumer-side SQL metadata/conversion plumbing without introducing application policy or replacing existing ORM APIs. Nested eager loading and flat joins remain separate tools with explicit cardinality. Precision-safe defaults avoid silent numeric/date round-trips; applications can opt into their own output parser.

Cursors are positions, not authorization or snapshots: consumers must reapply access filters and choose indexes. This PR makes no measured Xpenser performance claim; application adoption and representative performance checks follow publication.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactor / internal improvement

Blog post

Skipped the product blog: this is a Framework library/API change, with consumer-facing package and API-site documentation instead. No Xpenser feature is being shipped in this PR.

Screenshots / preview evidence

Not applicable: no application UI change or PR preview deployment is configured in this repository. Executable consumer/type examples and isolated PostgreSQL tests exercise the library behavior directly.

Integration coverage includes scoped left joins, grouped HAVING, all aggregate families and null/empty results, exact large decimals, parent/child cardinality, bound ordering and projection aliases, tracking isolation, transaction visibility, tied microsecond timestamps, mixed cursor directions, composite keys, required relations, and filter-scope preservation.

Validation

  • npm ci --no-audit --no-fund
  • npm run lint
  • npm run build
  • npm run typecheck:schema-site
  • npm run typecheck:docs-site
  • npm run test — 4,320 tests / 184 files, no type errors
  • npm run test:queries:integration with isolated PostgreSQL 16 — 25 passed
  • npm run docs — generated 826 HTML files, 0 errors; 242 non-fatal TypeDoc reference/highlighting warnings remain across the monorepo.
  • git diff --check
  • npm pack --workspace @cleverbrush/knex-schema --dry-run --json — consumer guide included in published files
  • GitHub: Lint, Build & Test (Node 24) — passed for dbca5c28
  • GitHub: PostgreSQL Query Integration — passed for dbca5c28
  • Preview/browser QA: not applicable (library-only; no preview workflow).
  • SigNoz: not applicable (no deployed service changed).
  • Telegram notification: skipped; the notifier rejects library-only notifications without an absolute preview/deployment URL (HTTP 400). No URL was invented.

Checklist

  • I've added tests for my changes
  • I've run npm run lint and fixed any issues
  • I've run npm run test and all tests pass
  • I've added a changeset if this changes package behavior
  • Changeset bumps are minor, not major

@andrewzolotukhin andrewzolotukhin left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

One general comment: add JSDoc to all public class members and exported functions, so consumer can understand why do we need them.

Comment on lines -799 to 929
export function query<
TLocalSchema extends ObjectSchemaBuilder<any, any, any, any, any, any, any>
>(
export function query(
knex: Knex,
schema: TLocalSchema,
schema: any,
baseQuery?: Knex.QueryBuilder
): SchemaQueryBuilder<TLocalSchema, QueryResultType<TLocalSchema>> {
return new SchemaQueryBuilder<TLocalSchema, QueryResultType<TLocalSchema>>(
knex,
schema,
baseQuery
);
): any {
if (isTableAlias(schema)) return new AliasedQueryBuilder(knex, schema);
return new SchemaQueryBuilder(knex, schema, baseQuery);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

do we miss some typings here? I want to have IDE hints and type checks everywhere, so this looks suspicious. Seems like we downgraded from strongly typed TSourceSchema to any.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in dbca5c2. The query() implementation now retains the source schema/alias generics and explicitly returns the corresponding typed builder instead of accepting/returning any. createQuery() now has real overloads and a checked satisfies BoundQuery construction, without the factory cast. Added compile-time regressions for ordinary and aliased calls, optional base queries, both transaction helpers, selected fields, left-join nullability, invalid property access, and ORM re-exports. Local full suite passes: 4,320 tests, no type errors; PostgreSQL transaction cases also pass.

Comment thread libs/knex-schema/src/aliased-query.ts Outdated
schema: S,
name: N
): TableAlias<S, N> {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

maybe let's extract this check to somewhere, I feel that checking the string to be a valid SQL identifier could be a common task.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in dbca5c2. Extracted and exported isSqlIdentifier(value: unknown): value is string and reused it in alias(). It intentionally validates the existing conservative ASCII single-name format, not every dialect's identifier grammar; qualified/quoted names and non-string values are rejected without coercion. JSDoc explains that identifier bindings are still required. Added dedicated validation and alias-integration tests, plus type narrowing coverage.

Comment on lines +36 to +37
eq(t.task.ownerId, t.owner.id),
or(eq(t.task.ownerId, t.owner.id))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I feel that syntax like:

                    or(eq(t.task.ownerId, t.owner.id), 
                     eq(t.task.ownerId, t.owner.id))

is more expressive, can we support it? With multiple nestings if needed (to support complex conditions).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes—or(eq(...), eq(...)) and arbitrary nested and()/or() groups were already supported by the recursive predicate renderer. In dbca5c2 I replaced the unhelpful example with meaningful alternatives, added a three-level AND/OR SQL-parenthesization regression and a real PostgreSQL result test, and documented the syntax. Empty groups explicitly throw. All 25 PostgreSQL cases pass locally.

Comment thread libs/orm/src/dbset.ts Outdated
Comment on lines +87 to +89
import('@cleverbrush/schema').InferType<
RelatedSchema<TEntity, K>
>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

do we really need this import() here or we can just use import type at the top of the file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in dbca5c2: added one top-level import type { InferType } from '@cleverbrush/schema' and replaced all three inline type-import expressions in this file. No runtime import or behavior change; build and type tests pass.

@andrewzolotukhin

Copy link
Copy Markdown
Contributor Author

Implemented the review follow-up in dbca5c2, including the package-wide JSDoc request from the review.

  • Public functions, class members and overloads across both knex-schema and orm now carry JSDoc, including existing APIs, transaction factories, aggregate methods, cursor/write options and error classes. Private/internal and third-party APIs are excluded.
  • Added regression checks against the emitted .d.ts files, so documentation is verified in the declarations installed consumers actually use.
  • Replied individually to all four inline comments with the corresponding typing, identifier-helper, nested-predicate and type-import changes.

Validation is complete: 4,320 tests / 184 files, no type errors; 25 PostgreSQL integration tests. Lint, build and both documentation-site typechecks pass. API docs generate 826 HTML files with 0 errors (242 non-fatal reference/highlighting warnings remain across the monorepo).

Both GitHub checks passed on the new commit. Changesets remain minor-only. No Xpenser changes or consumer-proposal document changes are included. No preview/SigNoz applies to this library-only PR; Telegram remains skipped because the notifier requires a preview/deployment URL. Ready for re-review; not merged.

@andrewzolotukhin
andrewzolotukhin merged commit d0bb389 into development Sep 21, 2026
2 checks passed
@andrewzolotukhin
andrewzolotukhin deleted the feat/composable-typed-queries branch September 21, 2026 13:18
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.

1 participant