Skip to content

refactor: unify adapter application into execute(query, target) - #754

Merged
tada5hi merged 12 commits into
masterfrom
refactor-adapter-execute-api
Jul 10, 2026
Merged

refactor: unify adapter application into execute(query, target)#754
tada5hi merged 12 commits into
masterfrom
refactor-adapter-execute-api

Conversation

@tada5hi

@tada5hi tada5hi commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Motivation

The SQL/TypeORM adapters carried a QUERY generic and a withQuery(query) method — but that "query" was not the rapiq IQuery AST. It was the backend object the adapter mutates (a TypeORM SelectQueryBuilder). So the usage site read confusingly, with "query" meaning two opposite things two lines apart:

adapter.withQuery(queryBuilder);          // the backend target
query.accept(new QueryVisitor(adapter));  // the rapiq Query AST
adapter.execute();

On top of the naming collision, applying a query was a three-step ritual with invisible ordering constraints, and QueryVisitor leaked into every usage site.

Change

Fold the whole apply sequence into a single entry point; the backend object becomes a TypeORM-local constructor option:

// @rapiq/sql — no backend object at all, execute returns the fragments
const fragments = adapter.execute(query);

// @rapiq/typeorm — builder bound at construction
const adapter = new TypeormAdapter({ queryBuilder });
const { pagination } = adapter.execute(query);

@rapiq/sql

  • IAdapter split into ISubAdapter (per-parameter contract: execute / clear) and IRootAdapter<OUTPUT>.
  • IRootAdapter.execute(query, options?) walks the query (constructs the QueryVisitor internally — no runtime cycle, the visitor's adapter imports are type-only) and returns the result. QueryVisitor is an implementation detail now; options.visitor remains as the escape hatch.
  • Adapter.execute(query) returns SqlFragments; build() is removed (it was the only way to read fragments before; execute now returns them).
  • The QUERY/TARGET generic is dropped entirely — plain SQL has no backend object to mutate; it emits fragments. The old Adapter<QUERY extends Record<string, any>> was dead weight.
  • QueryVisitor forwards its options to the sub-visitors (previously stored but never forwarded).

@rapiq/typeorm

  • The SelectQueryBuilder is bound at construction: new TypeormAdapter({ queryBuilder }). queryBuilder is required — previously an adapter without a builder compiled fine and every execute() was a silent no-op; the sub-adapters lost all their if (!this.queryBuilder) guards along with it.
  • TypeormAdapter.execute(query) walks + applies in one call, returning the applied pagination (e.g. for the response meta block).
  • Pagination is applied unconditionally on each run, so a re-run whose query drops pagination resets take/skip instead of leaking the previous run's values.
  • Adapters are per-request objects, like the builder they wrap — docs now say this explicitly; re-run/clear semantics are scoped to single-use-per-builder (builder-side mutations can't be undone by clear()).

Both

  • New ExecuteOptions { clear?: boolean (default true), visitor? }. execute clears accumulated state by default (self-contained calls); { clear: false } accumulates across calls (apply several queries onto one target). visitor forwards to the QueryVisitor/sub-visitors.

⚠️ Breaking changes (public API)

Before After
adapter.withQuery(target) (removed — TypeORM: pass queryBuilder in constructor options; SQL: no target concept)
query.accept(new QueryVisitor(adapter)) + adapter.execute() adapter.execute(query, options?)
Adapter.build() Adapter.execute(query) returns SqlFragments
IAdapter<QUERY> ISubAdapter (execute/clear) / IRootAdapter<OUTPUT>
generic QUERY on adapters/sub-adapters (removed)
new TypeormAdapter() (builder optional, attached later) new TypeormAdapter({ queryBuilder }) (required)
resolveQueryDialect(query?) resolveQueryDialect(query) (unbound case no longer exists)

Committed as refactor: (not feat!/BREAKING CHANGE:) so release-please does not auto-bump a major — the version decision is intentionally left to the maintainer.

Tests & docs

  • @rapiq/sql 69/69, @rapiq/typeorm 45/45 passing; both build clean; lint clean.
  • Added sql coverage for the new clear default (idempotent re-run) and { clear: false } accumulation.
  • TypeORM sub-visitor specs migrated to execute(new Query({ ... })) on a constructor-bound adapter (full path) or the public sub-adapters (adapter.filters, …) for isolated cases; the "no bound builder falls back to pg" spec is gone with the scenario it covered.
  • Updated packages/docs (sql, typeorm, quick-start, landing-page snippet) and READMEs.

Notes / follow-ups

  • RootAliasFn + toRootAliasFn/isRootAliasFn (@rapiq/sql) are exported but unused anywhere in the monorepo — they look like dead code worth removing in a follow-up.

Summary by CodeRabbit

  • New Features
    • Unified adapter flow: execute(query, options) now drives query application for both SQL fragments and TypeORM, with consistent per-call clearing behavior.
    • TypeORM adapter now binds the TypeORM queryBuilder at construction and returns applied pagination from execute(query).
  • Bug Fixes
    • Visitor options are applied consistently during query traversal.
    • Adapter state is cleared by default between calls to prevent cross-run leakage.
  • Documentation
    • Updated quick-start, integration docs, READMEs, and migration/architecture references to the new execute(query) pattern.
  • Tests
    • Refreshed unit/acceptance tests for the new execution flow and state clearing/accumulation behavior.

The adapter's `QUERY` generic named the backend object it mutates (e.g. a
TypeORM `SelectQueryBuilder`), not the rapiq `IQuery` AST — so `withQuery()`
read as attaching a rapiq query when it attached the target. Fold the whole
apply sequence into one entry point and rename the concept to `target`.

Before:
    adapter.withQuery(queryBuilder);
    query.accept(new QueryVisitor(adapter));
    const out = adapter.execute();

After:
    const out = adapter.execute(query, queryBuilder);

- @rapiq/sql: split `IAdapter` into `ISubAdapter` (per-parameter contract:
  `setTarget`/`execute`/`clear`) and `IRootAdapter<TARGET, OUTPUT>` whose
  `execute(query, target?, options?)` walks the query and returns the result.
  `Adapter.execute(query)` returns `SqlFragments`; `build()` is removed.
- @rapiq/typeorm: `TypeormAdapter.execute(query, queryBuilder)` walks and
  applies in a single call, returning the applied pagination.
- The `QUERY` type parameter is renamed `TARGET`; the internal `withQuery`
  setter becomes `setTarget`.
- Add `ExecuteOptions { clear?: boolean (default true), visitor? }`; execute
  now clears accumulated state by default (re-runnable) and forwards
  `visitor` options through `QueryVisitor` to the sub-visitors.

Docs and tests updated to the new entry point.
Copilot AI review requested due to automatic review settings July 8, 2026 15:48

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 reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR refactors @rapiq/sql and @rapiq/typeorm adapters from QUERY-centric state propagation to target-bound execute(query, options) flows, replacing withQuery/build patterns and updating contracts, tests, examples, and documentation.

Changes

Target-based execute refactor

Layer / File(s) Summary
SQL contracts
packages/sql/src/adapter/types.ts, packages/sql/src/adapter/{fields,filters,pagination,relations,sort}/types.ts, packages/sql/src/types.ts, packages/sql/src/helpers/root-alias.ts
Adds ISubAdapter and ExecuteOptions, removes IAdapter, and replaces query-typed adapter contracts with non-generic interfaces.
SQL adapter execution
packages/sql/src/adapter/{fields,filters,pagination,relations,sort}/{base,module}.ts, packages/sql/src/adapter/module.ts, packages/sql/src/visitor/module.ts
Removes stored query state and withQuery, and changes Adapter.execute(query, options) to visit queries, clear state, and return SqlFragments.
TypeORM target contracts and execution
packages/typeorm/src/adapter/types.ts, packages/typeorm/src/adapter/{fields,filters,pagination,relations,sort}.ts, packages/typeorm/src/adapter/module.ts, packages/typeorm/src/dialect.ts
Requires a constructor-bound SelectQueryBuilder, applies adapter state directly to it, and returns pagination from execute(query, options).
Validation and usage updates
packages/sql/test/unit/adapter.spec.ts, packages/typeorm/test/unit/**, packages/sql/README.md, packages/typeorm/README.md, packages/docs/**, .agents/**, README.md
Updates tests and documentation to use direct query execution, per-request adapter instances, default clearing, and optional state accumulation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant TypeormAdapter
  participant QueryVisitor
  participant SelectQueryBuilder

  Caller->>TypeormAdapter: execute(query, options)
  TypeormAdapter->>TypeormAdapter: clear state when options.clear
  TypeormAdapter->>QueryVisitor: visit query with visitor options
  QueryVisitor->>SelectQueryBuilder: apply fields, filters, pagination, relations, and sort
  TypeormAdapter-->>Caller: return pagination
Loading

Possibly related PRs

  • tada5hi/rapiq#700: Introduces the core query and interface typing used by the new execute contracts.
  • tada5hi/rapiq#741: Also changes SQL fragment assembly and root adapter execution semantics.
  • tada5hi/rapiq#743: Overlaps with TypeORM execution and dialect handling changes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: consolidating adapter application into execute(query) with a bound target.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor-adapter-execute-api

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

Comment @coderabbitai help to get the list of available commands.

The migrated clear test asserted idempotency (execute twice, same query ->
equal), which cannot catch a broken sort/pagination/relations clear() because
those sub-adapters overwrite/dedupe and reproduce the same output on a re-walk.
Add a test that a second execute() dropping those parameters returns empty
orderBy/limit/offset/relations, restoring the guarantee the pre-refactor
'reset on clear' test provided. Also assert column doubling in the clear:false
accumulation test.
@tada5hi
tada5hi force-pushed the refactor-adapter-execute-api branch from 96fe2ab to 5ba7838 Compare July 8, 2026 16:27
tada5hi added 8 commits July 8, 2026 18:30
The package READMEs (published to npm) and the root README still showed the
old withQuery() + external QueryVisitor walk + no-arg execute()/build() flow,
whose code samples no longer compile. Update them (and the living .agents docs
architecture.md, references/typeorm.md, migration-notes.md) to the single
execute(query, target) entry point. Historical .agents/plans/* records are
left as-is.
The adapter accumulates per-call state, so it is not a long-lived singleton:
the shareable unit is the options object, and the instance is constructed per
request (like the SelectQueryBuilder it is handed). Note this in the typeorm/sql
integration guides and package READMEs to preempt the reuse-vs-single-use
question.
…ute()

execute(query, target?, options?) forced a target param that @rapiq/sql
ignored entirely (it produces fragments). Move the write target into the
constructor options behind a shared BaseAdapterOptions<TARGET> { target? }
that both AdapterOptions (sql) and TypeormAdapterOptions extend, so execute is
now the uniform execute(query, options?) on both adapters.

- target is bound once at construction and fanned out to the sub-adapters;
  execute() no longer takes or re-binds it.
- target stays optional: @rapiq/sql ignores it, @rapiq/typeorm keeps its
  no-target -> postgres-preset fallback.
- per-request construction remains the model; share the config object and
  spread it with the request's builder: new TypeormAdapter({ ...config, target }).

Tests, integration docs and READMEs updated to the constructor-bound target.
The TARGET generic threaded through the whole adapter layer, but it was dead in
@rapiq/sql (which never touches a target — it emits fragments) and only earned
its keep typing TypeORM's builder. It also lived in every interface purely
because the old setTarget() signature mentioned it.

Stance B: model the write target as a TypeORM-only concern.

- @rapiq/sql: no target, no setTarget, no TARGET generic, no BaseAdapterOptions.
  ISubAdapter { execute; clear } (non-generic), IRootAdapter<OUTPUT>,
  execute(query, options?), AdapterOptions = DialectOptions & { rootAlias? }.
  The base classes are pure accumulation machinery.
- @rapiq/typeorm: each sub-adapter declares its own
  protected target?: SelectQueryBuilder<any> (fixed type, no generic) set from a
  constructor param; the root threads options.target into the five sub-adapter
  constructors. TypeormAdapterOptions { target?, relations? }. No generic.

The SelectQueryBuilder<User> entity-narrowing was ceremony; fixed <any> keeps
full builder-API type-checking inside TypeORM (incl. relations.ts expressionMap
navigation). Docs/agent guides updated; acceptance spec drops the generic arg.
Now that the write target is a TypeORM-local concept (@rapiq/sql has none),
name it for what it is. TypeormAdapterOptions.target -> queryBuilder, and the
per-adapter this.target field + onJoin's third parameter follow. Usage becomes
new TypeormAdapter({ queryBuilder, relations }).execute(query).

Tests, integration docs and READMEs updated to the shorthand form.
…e/skip

PaginationAdapter.execute() only applied take/skip behind if(this.limit)/
if(this.offset) guards, so a second execute() (default clear:true) whose query
drops pagination left the previous run's take/skip on the builder — wrong
LIMIT/OFFSET, contradicting the clear-is-re-runnable contract that sort/filters
already honor. Apply take/skip unconditionally (falsy -> undefined = no clause),
matching SortAdapter. Add a re-run regression test.

Also: align PaginationAdapter's constructor signature with the other four
sub-adapters, and drop the stale 'target bound at construction' JSDoc on the
@rapiq/sql Adapter.execute() (sql has no target).
'an adapter instance is re-runnable' overpromised: clear resets the adapter's
own accumulated state, but the bound queryBuilder is mutated in place (joins and
the selected projection do not roll back). Reword to point at per-query
construction and reserve { clear: false } for accumulating onto one builder.
An adapter constructed without a builder was permanently a silent
no-op (every sub-adapter guarded on the builder's presence). Now that
the builder can only be bound via the constructor, make it required:
the option loses its '?', the sub-adapters drop their undefined
guards, and resolveQueryDialect no longer models the unbound case.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/typeorm/src/adapter/filters.ts`:
- Around line 59-63: In execute(), guard the result of getQueryAndParameters()
before calling queryBuilder.where(): when no active filters produce an empty SQL
string, skip where() or return undefined so cleared reruns do not emit invalid
SQL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75068b1c-1bcf-49d0-9302-9154d5edbb71

📥 Commits

Reviewing files that changed from the base of the PR and between f36c964 and 50c14cf.

📒 Files selected for processing (12)
  • packages/docs/integrations/typeorm.md
  • packages/sql/src/adapter/module.ts
  • packages/typeorm/src/adapter/fields.ts
  • packages/typeorm/src/adapter/filters.ts
  • packages/typeorm/src/adapter/module.ts
  • packages/typeorm/src/adapter/pagination.ts
  • packages/typeorm/src/adapter/relations.ts
  • packages/typeorm/src/adapter/sort.ts
  • packages/typeorm/src/adapter/types.ts
  • packages/typeorm/src/dialect.ts
  • packages/typeorm/test/unit/adapter/filters.spec.ts
  • packages/typeorm/test/unit/adapter/module.spec.ts
💤 Files with no reviewable changes (1)
  • packages/typeorm/test/unit/adapter/filters.spec.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/typeorm/src/adapter/types.ts
  • packages/docs/integrations/typeorm.md
  • packages/typeorm/src/adapter/module.ts
  • packages/sql/src/adapter/module.ts

Comment thread packages/typeorm/src/adapter/filters.ts
tada5hi added 2 commits July 10, 2026 11:46
typeorm's where() clears expressionMap.wheres before adding a
condition and skips falsy ones, so the adapter's unconditional
where('', []) call is what resets a stale WHERE on re-run — and it
emits no dangling clause. Guarding it on non-empty sql (as suggested
in review) would leak the previous run's WHERE.
queryBuilder is required at construction now, so the only remaining
pg-fallback case is an unknown connection type.
@tada5hi
tada5hi merged commit ff80070 into master Jul 10, 2026
6 checks passed
@tada5hi
tada5hi deleted the refactor-adapter-execute-api branch July 27, 2026 07:53
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