From 3ebacfe10634c155fe316f45bc1562d45eca5cea Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 15:04:04 -0600 Subject: [PATCH 01/32] feat(db): carry join compilation on imported join operators Joins take an operator value: .join(leftJoin(source, on)). The operator carries the join compiler and the optimizer's join pushdown onto the IR join clause, so apps without joins do not bundle them. Co-authored-by: Isaac --- packages/db/src/query/builder/index.ts | 135 +---- .../db/src/query/builder/join-operators.ts | 67 +++ packages/db/src/query/compiler/index.ts | 6 +- packages/db/src/query/compiler/joins.ts | 33 +- packages/db/src/query/compiler/lazy-demand.ts | 32 ++ packages/db/src/query/index.ts | 11 + packages/db/src/query/ir.ts | 7 + packages/db/src/query/optimizer-joins.ts | 498 ++++++++++++++++++ packages/db/src/query/optimizer.ts | 486 +---------------- .../db/tests/collection-auto-index.test.ts | 16 +- .../collection-cleanup-restart-oracle.test.ts | 21 +- ...ction-query-publication-boundaries.test.ts | 4 +- ...ction-subscriber-duplicate-inserts.test.ts | 7 +- packages/db/tests/conformance/suite.ts | 25 +- packages/db/tests/effect.test.ts | 57 +- ...-query-deferred-acquisition-oracle.test.ts | 5 +- packages/db/tests/live-query-options.test.ts | 19 +- .../query/aggregate-value-contracts.test-d.ts | 7 +- .../tests/query/async-hold-outcomes.test.ts | 12 +- .../db/tests/query/builder/buildQuery.test.ts | 12 +- .../query/builder/callback-types.test-d.ts | 51 +- packages/db/tests/query/builder/from.test.ts | 3 +- .../query/builder/functional-variants.test.ts | 25 +- packages/db/tests/query/builder/join.test.ts | 128 ++--- .../tests/query/builder/subqueries.test-d.ts | 21 +- .../db/tests/query/builder/union-all.test.ts | 15 +- packages/db/tests/query/case-when.test-d.ts | 16 +- packages/db/tests/query/case-when.test.ts | 25 +- .../cold-join-reconciliation-oracle.test.ts | 56 +- .../collection-ref-compatibility.test.ts | 4 +- .../db/tests/query/compiler/basic.test.ts | 4 +- .../tests/query/compiler/lazy-demand.test.ts | 4 +- .../tests/query/compiler/subqueries.test.ts | 16 +- .../query/compiler/subquery-caching.test.ts | 8 +- packages/db/tests/query/composables.test.ts | 21 +- packages/db/tests/query/distinct.test.ts | 8 +- .../db/tests/query/findone-joins.test-d.ts | 61 +-- .../tests/query/functional-variants.test-d.ts | 27 +- .../tests/query/functional-variants.test.ts | 17 +- .../tests/query/generic-collection.test-d.ts | 7 +- packages/db/tests/query/group-by.test.ts | 7 +- .../identity-output-shape-oracle.test.ts | 23 +- .../includes-alias-shadowing-oracle.test.ts | 175 +++--- ...ncludes-collection-oracle.property.test.ts | 23 +- .../includes-context-transport-oracle.test.ts | 110 ++-- ...-cross-formulation-oracle.property.test.ts | 18 +- .../tests/query/includes-lazy-loading.test.ts | 12 +- ...ludes-outer-join-route-work-oracle.test.ts | 14 +- .../query/includes-publication-oracle.test.ts | 16 +- .../query/includes-query-shape-oracle.test.ts | 21 +- .../query/includes-scope-identity-oracle.ts | 54 +- .../query/includes-temporal-oracle.test.ts | 15 +- .../includes-work-counter-oracle.test.ts | 21 +- packages/db/tests/query/includes.test.ts | 14 +- packages/db/tests/query/indexes.test.ts | 53 +- .../query/ir-stable-identity-oracle.test.ts | 21 +- .../join-result-key-oracle.property.test.ts | 4 +- .../db/tests/query/join-subquery.test-d.ts | 58 +- packages/db/tests/query/join-subquery.test.ts | 186 +++---- packages/db/tests/query/join.test-d.ts | 196 +++---- packages/db/tests/query/join.test.ts | 248 +++------ .../tests/query/live-query-collection.test.ts | 71 +-- .../query/load-subset-join-dedupe.test.ts | 7 +- ...ad-subset-replay-refinement-oracle.test.ts | 29 +- ...source-readiness-refinement-oracle.test.ts | 7 +- .../tests/query/load-subset-subquery.test.ts | 24 +- .../query/optimizer-semantics-oracle.test.ts | 65 ++- packages/db/tests/query/optimizer.test.ts | 84 +-- .../query/optional-fields-negative.test-d.ts | 16 +- .../query/optional-fields-runtime.test.ts | 9 +- packages/db/tests/query/order-by.test.ts | 18 +- .../ordered-lifecycle-oracle.property.test.ts | 30 +- .../ordered-work-oracle.property.test.ts | 89 ++-- .../query/pagination-oracle.property.test.ts | 22 +- .../query/query-api-type-algebra.test-d.ts | 38 +- .../query/query-api-type-algebra.test.ts | 17 +- packages/db/tests/query/query-once.test.ts | 9 +- .../tests/query/query-while-syncing.test.ts | 35 +- packages/db/tests/query/scheduler.test.ts | 44 +- packages/db/tests/query/select-spread.test.ts | 4 +- .../query/subquery-user-value-oracle.test.ts | 66 +-- .../tests/query/subset-error-matrix.test.ts | 27 +- packages/db/tests/query/union-all.test-d.ts | 53 +- packages/db/tests/query/union-all.test.ts | 87 ++- .../db/tests/query/validate-aliases.test.ts | 55 +- .../query/virtual-row-fields-oracle.test-d.ts | 18 +- .../query/virtual-row-fields-oracle.test.ts | 18 +- 87 files changed, 1763 insertions(+), 2445 deletions(-) create mode 100644 packages/db/src/query/builder/join-operators.ts create mode 100644 packages/db/src/query/compiler/lazy-demand.ts create mode 100644 packages/db/src/query/optimizer-joins.ts diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index 992192a539..ec524a09cb 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -25,7 +25,6 @@ import { QueryMustHaveFromClauseError, SubQueryMustHaveFromClauseError, } from '../../errors.js' -import { validateJoinConditions } from '../join-conditions.js' import { codedMessage, devBuild } from '../../error-message.js' import { getQueryIR } from './query-ir.js' import { cloneQueryForPlacement } from './clone-query.js' @@ -42,6 +41,7 @@ import { ToArrayWrapper, } from './functions.js' import type { CollectionResolver } from './clone-query.js' +import type { JoinOperator, JoinType } from './join-operators.js' import type { SourceClauseContext } from '../../errors.js' import type { NamespacedRow, SingleResult } from '../../types.js' import type { @@ -64,7 +64,6 @@ import type { FunctionalHavingRow, GetResult, GroupByCallback, - JoinOnCallback, MergeContextForJoinCallback, MergeContextWithJoinType, NonScalarSelectObject, @@ -334,19 +333,15 @@ export class BaseQueryBuilder { * .from({ activeUsers }) * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId)) */ - join< - TSource extends Source, - TJoinType extends `inner` | `left` | `right` | `full` = `left`, - >( - source: TSource, - onCallback: JoinOnCallback< - MergeContextForJoinCallback> - >, - type: TJoinType = `left` as TJoinType, + join( + operator: JoinOperator, ): QueryBuilder< MergeContextWithJoinType, TJoinType> > { - const [alias, from] = this._createRefForSource(source, `join clause`) + const [alias, from] = this._createRefForSource( + operator.source, + `join clause`, + ) // Create a temporary context for the callback const currentAliases = this._getCurrentAliases() @@ -359,10 +354,16 @@ export class BaseQueryBuilder { > // Get the join condition expression - const onExpression = onCallback(refProxy) + const onExpression = operator.on(refProxy) - validateJoinConditions(onExpression) - const joinClause: JoinClause = { from, type, on: onExpression } + operator.validate(onExpression) + const joinClause: JoinClause = { + from, + type: operator.type, + on: onExpression, + compile: operator.compile, + optimize: operator.optimize, + } const existingJoins = this.query.join || [] @@ -372,110 +373,6 @@ export class BaseQueryBuilder { }) as any } - /** - * Perform a LEFT JOIN with another table or subquery - * - * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery - * @param onCallback - A function that receives table references and returns the join condition - * @returns A QueryBuilder with the left joined table available - * - * @example - * ```ts - * // Left join users with posts - * query - * .from({ users: usersCollection }) - * .leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) - * ``` - */ - leftJoin( - source: TSource, - onCallback: JoinOnCallback< - MergeContextForJoinCallback> - >, - ): QueryBuilder< - MergeContextWithJoinType, `left`> - > { - return this.join(source, onCallback, `left`) - } - - /** - * Perform a RIGHT JOIN with another table or subquery - * - * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery - * @param onCallback - A function that receives table references and returns the join condition - * @returns A QueryBuilder with the right joined table available - * - * @example - * ```ts - * // Right join users with posts - * query - * .from({ users: usersCollection }) - * .rightJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) - * ``` - */ - rightJoin( - source: TSource, - onCallback: JoinOnCallback< - MergeContextForJoinCallback> - >, - ): QueryBuilder< - MergeContextWithJoinType, `right`> - > { - return this.join(source, onCallback, `right`) - } - - /** - * Perform an INNER JOIN with another table or subquery - * - * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery - * @param onCallback - A function that receives table references and returns the join condition - * @returns A QueryBuilder with the inner joined table available - * - * @example - * ```ts - * // Inner join users with posts - * query - * .from({ users: usersCollection }) - * .innerJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) - * ``` - */ - innerJoin( - source: TSource, - onCallback: JoinOnCallback< - MergeContextForJoinCallback> - >, - ): QueryBuilder< - MergeContextWithJoinType, `inner`> - > { - return this.join(source, onCallback, `inner`) - } - - /** - * Perform a FULL JOIN with another table or subquery - * - * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery - * @param onCallback - A function that receives table references and returns the join condition - * @returns A QueryBuilder with the full joined table available - * - * @example - * ```ts - * // Full join users with posts - * query - * .from({ users: usersCollection }) - * .fullJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) - * ``` - */ - fullJoin( - source: TSource, - onCallback: JoinOnCallback< - MergeContextForJoinCallback> - >, - ): QueryBuilder< - MergeContextWithJoinType, `full`> - > { - return this.join(source, onCallback, `full`) - } - /** * Filter rows based on a condition * diff --git a/packages/db/src/query/builder/join-operators.ts b/packages/db/src/query/builder/join-operators.ts new file mode 100644 index 0000000000..c95f6ef8c4 --- /dev/null +++ b/packages/db/src/query/builder/join-operators.ts @@ -0,0 +1,67 @@ +import { processJoins } from '../compiler/joins.js' +import { validateJoinConditions } from '../join-conditions.js' +import { applyOptimizations } from '../optimizer-joins.js' +import type { + Context, + JoinOnCallback, + MergeContextForJoinCallback, + SchemaFromSource, + Source, +} from './types.js' + +export type JoinType = `inner` | `left` | `right` | `full` + +/** + * A join step for `query.join(...)`. Importing a join operator is what brings + * the join compiler into a bundle. + */ +export interface JoinOperator< + TContext extends Context, + TSource extends Source, + TJoinType extends JoinType, +> { + source: TSource + on: JoinOnCallback< + MergeContextForJoinCallback> + > + type: TJoinType + compile: typeof processJoins + optimize: typeof applyOptimizations + validate: typeof validateJoinConditions +} + +function joinOperator(type: TJoinType) { + return ( + source: TSource, + on: JoinOnCallback< + MergeContextForJoinCallback> + >, + ): JoinOperator => ({ + source, + on, + type, + compile: processJoins, + optimize: applyOptimizations, + validate: validateJoinConditions, + }) +} + +export const leftJoin = joinOperator(`left`) +export const rightJoin = joinOperator(`right`) +export const innerJoin = joinOperator(`inner`) +export const fullJoin = joinOperator(`full`) + +/** A join whose type is chosen at run time. */ +export function joinOf< + TJoinType extends JoinType, + TContext extends Context, + TSource extends Source, +>( + type: TJoinType, + source: TSource, + on: JoinOnCallback< + MergeContextForJoinCallback> + >, +): JoinOperator { + return joinOperator(type)(source, on) +} diff --git a/packages/db/src/query/compiler/index.ts b/packages/db/src/query/compiler/index.ts index 7fa3d8deac..a19835d712 100644 --- a/packages/db/src/query/compiler/index.ts +++ b/packages/db/src/query/compiler/index.ts @@ -66,7 +66,7 @@ import { isCaseWhenConditionTrue, toBooleanPredicate, } from './evaluators.js' -import { processJoins, registerLazyDemandPlan } from './joins.js' +import { registerLazyDemandPlan } from './lazy-demand.js' import { containsAggregate, processGroupBy } from './group-by.js' import { getLazyLoadTargets } from './lazy-targets.js' import { processOrderBy } from './order-by.js' @@ -99,7 +99,7 @@ import type { QueryRef, UnionAll, } from '../ir.js' -import type { LazyCollectionCallbacks } from './joins.js' +import type { LazyCollectionCallbacks } from './lazy-demand.js' import type { IStreamBuilder, KeyValue, MultiSet } from '@tanstack/db-ivm' import type { Collection } from '../../collection/index.js' import type { @@ -591,7 +591,7 @@ function compileQueryInternal( // Process JOIN clauses if they exist if (query.join && query.join.length > 0) { - pipeline = processJoins( + pipeline = query.join[0]!.compile( pipeline, query.join, sources, diff --git a/packages/db/src/query/compiler/joins.ts b/packages/db/src/query/compiler/joins.ts index 65f1e38092..8456762246 100644 --- a/packages/db/src/query/compiler/joins.ts +++ b/packages/db/src/query/compiler/joins.ts @@ -29,6 +29,7 @@ import { } from '../ir.js' import { compileExpression } from './evaluators.js' import { getLazyLoadTargets } from './lazy-targets.js' +import { registerLazyDemandPlan } from './lazy-demand.js' import { crossJoinParentRoutes } from './parent-routes.js' import { queriesMatchForCaching } from './query-equivalence.js' import { @@ -40,6 +41,7 @@ import { getRoutedScalarMetadata, stripRouteMetadata, } from './route-metadata.js' +import type { LazyCollectionCallbacks } from './lazy-demand.js' import type { ValueIdentity } from '../equality-value-identity.js' import type { CompileQueryFn } from './index.js' import type { OrderByOptimizationInfo } from './order-by.js' @@ -62,18 +64,7 @@ import type { import type { QueryCache, QueryMapping, WindowOptions } from './types.js' import type { CollectionSubscription } from '../../collection/subscription.js' -export type LazyDemandPlan = { - id: string - path: Array - collectionId: string - initialKeys: Set -} - -/** Callbacks for managing lazy-loaded collections in optimized joins */ -export type LazyCollectionCallbacks = { - plans?: Array - setDemand?: (plan: LazyDemandPlan, keys: Set) => void -} +export type { LazyCollectionCallbacks, LazyDemandPlan } from './lazy-demand.js' type JoinInputValue = [ originalKey: unknown, @@ -81,8 +72,6 @@ type JoinInputValue = [ joinValue: unknown, ] -let nextLazyDemandPlanId = 0 - function parameterizeJoinInputByParentRoutes( input: KeyedStream, parentKeyStream: KeyedStream, @@ -191,22 +180,6 @@ function keyJoinInput( ] } -export function registerLazyDemandPlan( - callbacks: Record, - target: { sourceId: string; path: Array; collection: Collection }, - initialKeys: Set = new Set(), -): LazyDemandPlan { - const plan: LazyDemandPlan = { - id: `lazy-demand-${++nextLazyDemandPlanId}`, - path: target.path, - collectionId: target.collection.id, - initialKeys: new Set(initialKeys), - } - const state = (callbacks[target.sourceId] ??= {}) - ;(state.plans ??= []).push(plan) - return plan -} - /** * Processes all join clauses, applying lazy loading optimizations and maintaining * alias tracking for per-alias subscriptions (enables self-joins). diff --git a/packages/db/src/query/compiler/lazy-demand.ts b/packages/db/src/query/compiler/lazy-demand.ts new file mode 100644 index 0000000000..708fe4d23e --- /dev/null +++ b/packages/db/src/query/compiler/lazy-demand.ts @@ -0,0 +1,32 @@ +import type { Collection } from '../../collection/index.js' + +export type LazyDemandPlan = { + id: string + path: Array + collectionId: string + initialKeys: Set +} + +/** Callbacks for managing lazy-loaded collections in optimized joins */ +export type LazyCollectionCallbacks = { + plans?: Array + setDemand?: (plan: LazyDemandPlan, keys: Set) => void +} + +let nextLazyDemandPlanId = 0 + +export function registerLazyDemandPlan( + callbacks: Record, + target: { sourceId: string; path: Array; collection: Collection }, + initialKeys: Set = new Set(), +): LazyDemandPlan { + const plan: LazyDemandPlan = { + id: `lazy-demand-${++nextLazyDemandPlanId}`, + path: target.path, + collectionId: target.collection.id, + initialKeys: new Set(initialKeys), + } + const state = (callbacks[target.sourceId] ??= {}) + ;(state.plans ??= []).push(plan) + return plan +} diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index 3c104b813c..5d486bd9e9 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -35,6 +35,17 @@ export { type Prettify, } from './builder/index.js' +// Join operators +export { + leftJoin, + rightJoin, + innerJoin, + fullJoin, + joinOf, + type JoinOperator, + type JoinType, +} from './builder/join-operators.js' + // Expression functions exports export { // Operators diff --git a/packages/db/src/query/ir.ts b/packages/db/src/query/ir.ts index a1e9a67ea4..e2d25a227d 100644 --- a/packages/db/src/query/ir.ts +++ b/packages/db/src/query/ir.ts @@ -10,6 +10,9 @@ import type { Collection, CollectionImpl } from '../collection/index.js' import type { CollectionOptionsIdentity } from '../collection-options.js' import type { NamespacedRow } from '../types' +import type { processJoins } from './compiler/joins.js' +import type { applyOptimizations } from './optimizer-joins.js' + export interface QueryIR { from: From select?: Select @@ -49,6 +52,10 @@ export interface JoinClause { from: CollectionSourceRef | QueryRef type: `left` | `right` | `inner` | `outer` | `full` | `cross` on: BasicExpression + // The join operator value that built this clause supplies the compiler, + // so apps without joins never import it. + compile: typeof processJoins + optimize: typeof applyOptimizations } export type Where = diff --git a/packages/db/src/query/optimizer-joins.ts b/packages/db/src/query/optimizer-joins.ts new file mode 100644 index 0000000000..848667f92e --- /dev/null +++ b/packages/db/src/query/optimizer-joins.ts @@ -0,0 +1,498 @@ +import { + Func, + PropRef, + QueryRef as QueryRefClass, + UnionAll as UnionAllClass, + UnionFrom as UnionFromClass, + collectPropRefs, + createResidualWhere, + getWhereExpression, + isResidualWhere, +} from './ir.js' +import { containsAggregate } from './compiler/group-by.js' +import { + combineWithAnd, + getFirstFromAlias, + getLocalBindingIds, + getNullableJoinSources, + splitAndClausesRecursive, +} from './optimizer.js' +import type { GroupedWhereClauses } from './optimizer.js' +import type { + BasicExpression, + CollectionSourceRef, + From, + QueryIR, + Select, + Where, +} from './ir.js' + +// Predicate pushdown for queries with joins. Join operator values supply this +// to the optimizer, so apps without joins never import it. + +/** + * Step 4: Apply optimizations by lifting single-source clauses into subqueries. + * + * Creates a new QueryIR with single-source WHERE clauses moved to subqueries + * that wrap the original table references. This ensures immutability and prevents + * infinite recursion issues. + * + * @param query - Original QueryIR to optimize + * @param groupedClauses - WHERE clauses grouped by optimization strategy + * @returns New QueryIR with optimizations applied + */ +export function applyOptimizations( + query: QueryIR, + groupedClauses: GroupedWhereClauses, +): QueryIR { + // Track which single-source clauses were actually optimized + const actuallyOptimized = new Set() + + // Determine which source aliases are on the nullable side of outer joins. + const nullableSources = getNullableJoinSources(query) + + // Build a filtered copy of singleSource that excludes nullable-side clauses. + // Pushing a WHERE clause into the nullable side's subquery pre-filters the + // data before the join, converting "matched but WHERE-excluded" rows into + // "unmatched" outer-join rows. These are indistinguishable from genuinely + // unmatched rows, so the residual WHERE cannot correct the result. + const pushableSingleSource = new Map>() + for (const [source, clause] of groupedClauses.singleSource) { + if (!nullableSources.has(source)) { + pushableSingleSource.set(source, clause) + } + } + + // Optimize the main FROM clause and track what was optimized + const optimizedFrom = optimizeFromWithTracking( + query.from, + pushableSingleSource, + actuallyOptimized, + ) + + // Optimize JOIN clauses and track what was optimized + const optimizedJoins = query.join + ? query.join.map((joinClause) => ({ + ...joinClause, + from: optimizeFromWithTracking( + joinClause.from, + pushableSingleSource, + actuallyOptimized, + ) as CollectionSourceRef | QueryRefClass, + })) + : undefined + + // Keep regular and residual clauses separate so neither loses its marker. + const remainingWhereClauses: Array = [] + const residualWhereClauses = query.where?.filter(isResidualWhere) ?? [] + + // Add multi-source clauses + if (groupedClauses.multiSource) { + remainingWhereClauses.push(groupedClauses.multiSource) + } + + // Determine if we need residual clauses (when query has outer JOINs) + const hasOuterJoins = nullableSources.size > 0 + + // Add single-source clauses + for (const [source, clause] of groupedClauses.singleSource) { + if (!actuallyOptimized.has(source)) { + // Wasn't optimized at all - keep as regular WHERE clause + remainingWhereClauses.push(clause) + } else if (hasOuterJoins) { + // Was optimized AND query has outer JOINs - keep as residual WHERE clause + residualWhereClauses.push(createResidualWhere(clause)) + } + // If optimized and no outer JOINs - don't keep (original behavior) + } + + // Combine within each group to avoid extra filters and nested ANDs. + const combineRemaining = (clauses: Array): Where | undefined => + clauses.length > 1 + ? combineWithAnd( + clauses.flatMap((clause) => + splitAndClausesRecursive(getWhereExpression(clause)), + ), + ) + : clauses[0] + const regularWhere = combineRemaining(remainingWhereClauses) + const residualWhere = combineRemaining(residualWhereClauses) + const finalWhere: Array = [ + ...(regularWhere ? [regularWhere] : []), + ...(residualWhere + ? [createResidualWhere(getWhereExpression(residualWhere))] + : []), + ] + + // Preserve untouched query options while replacing the optimized clauses. + const optimizedQuery: QueryIR = { + ...copyClauseArrays(query), + from: optimizedFrom, + join: optimizedJoins, + where: finalWhere.length > 0 ? finalWhere : [], + } + + return optimizedQuery +} + +/** + * Helper function to create a deep copy of a QueryIR object for immutability. + * + * This ensures that all optimizations create new objects rather than modifying + * existing ones, preventing infinite recursion and shared reference issues. + * + * @param query - QueryIR to deep copy + * @returns New QueryIR object with all nested objects copied + */ +function deepCopyQuery(query: QueryIR): QueryIR { + return { + ...copyClauseArrays(query), + from: deepCopyFrom(query.from), + join: query.join + ? query.join.map((joinClause) => ({ + type: joinClause.type, + on: joinClause.on, + compile: joinClause.compile, + optimize: joinClause.optimize, + from: deepCopyFrom(joinClause.from) as + CollectionSourceRef | QueryRefClass, + })) + : undefined, + where: query.where ? [...query.where] : undefined, + } +} + +/** + * Helper function to optimize a FROM clause while tracking what was actually optimized. + * + * @param from - FROM clause to optimize + * @param singleSourceClauses - Map of source aliases to their WHERE clauses + * @param actuallyOptimized - Set to track which sources were actually optimized + * @returns New FROM clause, potentially wrapped in a subquery + */ +function optimizeFromWithTracking( + from: From, + singleSourceClauses: Map>, + actuallyOptimized: Set, +): From { + if (from.type === `unionFrom`) { + return new UnionFromClass( + from.sources.map( + (source) => + optimizeFromWithTracking( + source, + singleSourceClauses, + actuallyOptimized, + ) as CollectionSourceRef | QueryRefClass, + ), + ) + } + + if (from.type === `unionAll`) { + return new UnionAllClass( + from.queries.map((branch) => deepCopyQuery(branch)), + ) + } + + const whereClause = singleSourceClauses.get(from.alias) + + if (!whereClause) { + // No optimization needed. Keep the source ref itself: its sourceId is + // the compiled input identity, and a copy would lose it. + if (from.type === `collectionRef` || from.type === `descriptorRef`) { + return from + } + // Must be queryRef due to type system + return new QueryRefClass( + deepCopyQuery(from.query), + from.alias, + from.bindingId, + ) + } + + if (from.type === `collectionRef` || from.type === `descriptorRef`) { + // Create a new subquery with the WHERE clause for the collection + // This is always safe since we're creating a new subquery + const subQuery: QueryIR = { + from, + where: [whereClause], + } + actuallyOptimized.add(from.alias) // Mark as successfully optimized + return new QueryRefClass(subQuery, from.alias, from.bindingId) + } + + // SAFETY CHECK: Only check safety when pushing WHERE clauses into existing subqueries + // We need to be careful about pushing WHERE clauses into subqueries that already have + // aggregates, HAVING, or ORDER BY + LIMIT since that could change their semantics + if (!isSafeToPushIntoExistingSubquery(from.query, whereClause, from.alias)) { + // Return a copy without optimization to maintain immutability + // Do NOT mark as optimized since we didn't actually optimize it + return new QueryRefClass( + deepCopyQuery(from.query), + from.alias, + from.bindingId, + ) + } + + // Skip pushdown when a clause references a field that only exists via a renamed + // projection inside the subquery; leaving it outside preserves the alias mapping. + if (referencesAliasWithRemappedSelect(from.query, whereClause, from.alias)) { + return new QueryRefClass( + deepCopyQuery(from.query), + from.alias, + from.bindingId, + ) + } + + // Add the WHERE clause to the existing subquery + // Create a deep copy to ensure immutability + const existingWhere = from.query.where || [] + const remappedWhere = remapWhereForSubquery( + from.query, + whereClause, + from.alias, + ) + if (remappedWhere === undefined) { + return new QueryRefClass( + deepCopyQuery(from.query), + from.alias, + from.bindingId, + ) + } + const optimizedSubQuery: QueryIR = { + ...deepCopyQuery(from.query), + where: [...existingWhere, remappedWhere], + } + actuallyOptimized.add(from.alias) // Mark as successfully optimized + return new QueryRefClass(optimizedSubQuery, from.alias, from.bindingId) +} + +/** + * Rewrites references to an outer QueryRef alias so a pushed predicate can be + * evaluated inside the subquery's namespace. Pass-through SELECT fields use + * their projected source path; unprojected rows use the first source alias. + */ +function remapWhereForSubquery( + subquery: QueryIR, + whereClause: BasicExpression, + outerAlias: string, +): BasicExpression | undefined { + const firstFromAlias = getFirstFromAlias(subquery) + if (firstFromAlias === undefined) return undefined + + const remapExpression = (expression: BasicExpression): BasicExpression => { + if (expression instanceof PropRef) { + if (expression.path[0] !== outerAlias) return expression + + const field = expression.path[1] + const projected = field ? subquery.select?.[field] : undefined + const hasNamespacedResult = + subquery.join !== undefined || subquery.from.type === `unionFrom` + const innerPath = + projected instanceof PropRef + ? [...projected.path, ...expression.path.slice(2)] + : hasNamespacedResult + ? expression.path.slice(1) + : [firstFromAlias, ...expression.path.slice(1)] + + return new PropRef(innerPath) + } + + if (expression instanceof Func) { + return new Func(expression.name, expression.args.map(remapExpression)) + } + + return expression + } + + return remapExpression(whereClause) as BasicExpression +} + +function isSafeToPushIntoExistingSubquery( + query: QueryIR, + whereClause: BasicExpression, + outerAlias: string, +): boolean { + return !( + query.distinct || + (query.select && + (containsAggregate(query.select) || + whereReferencesComputedSelectFields( + query.select, + whereClause, + outerAlias, + ))) || + query.groupBy?.length || + query.having?.length || + (query.orderBy?.length && + (query.limit !== undefined || query.offset !== undefined)) || + query.fnSelect || + query.fnWhere?.length || + query.fnHaving?.length + ) +} + +/** + * Determines whether the provided WHERE clause references fields that are + * computed by a subquery SELECT rather than pass-through properties. + * + * If true, pushing the WHERE clause into the subquery could change semantics + * (since computed fields do not necessarily exist at the subquery input level), + * so predicate pushdown must be avoided. + * + * @param select - The subquery SELECT map + * @param whereClause - The WHERE expression to analyze + * @param outerAlias - The alias of the subquery in the outer query + * @returns True if WHERE references computed fields, otherwise false + */ +function whereReferencesComputedSelectFields( + select: Select, + whereClause: BasicExpression, + outerAlias: string, +): boolean { + // Build a set of computed field names at the top-level of the subquery output + const computed = new Set() + for (const [key, value] of Object.entries(select)) { + if (key.startsWith(`__SPREAD_SENTINEL__`)) continue + if (value instanceof PropRef) continue + // Nested object or non-PropRef expression counts as computed + computed.add(key) + } + + const refs = collectPropRefs(whereClause) + + for (const ref of refs) { + const path = (ref as any).path as Array + if (!Array.isArray(path) || path.length < 2) continue + const alias = path[0] + const field = path[1] as string + if (alias !== outerAlias) continue + if (computed.has(field)) return true + } + return false +} + +/** + * Detects whether a WHERE clause references the subquery alias through fields that + * are re-exposed under different names (renamed SELECT projections or fnSelect output). + * In those cases we keep the clause at the outer level to avoid alias remapping bugs. + * TODO: in future we should handle this by rewriting the clause to use the subquery's + * internal field references, but it likely needs a wider refactor to do cleanly. + */ +function referencesAliasWithRemappedSelect( + subquery: QueryIR, + whereClause: BasicExpression, + outerAlias: string, +): boolean { + const refs = collectPropRefs(whereClause) + // Only care about clauses that actually reference the outer alias. + if (refs.every((ref) => ref.path[0] !== outerAlias)) { + return false + } + + // fnSelect always rewrites the row shape, so alias-safe pushdown is impossible. + if (subquery.fnSelect) { + return true + } + + const select = subquery.select + // Without an explicit SELECT the clause still refers to the original collection. + if (!select) { + return false + } + const hasSpreadProjection = Object.keys(select).some((key) => + key.startsWith(`__SPREAD_SENTINEL__`), + ) + const localBindings = getLocalBindingIds(subquery) + + for (const ref of refs) { + const path = ref.path + // Need at least alias + field to matter. + if (path.length < 2) continue + if (path[0] !== outerAlias) continue + + const projected = select[path[1]!] + // A spread-selected field has no direct projection entry to remap. + // Keep its predicate outside rather than guessing its input source. + if (!projected) { + if (hasSpreadProjection) return true + continue + } + + // Non-PropRef projections are computed values; cannot push down. + if (!(projected instanceof PropRef)) { + return true + } + + // A selected ancestor field depends on the parent route. Moving its + // predicate into this source would read a same-named child field instead. + if ( + projected.bindingId !== undefined && + !localBindings.has(projected.bindingId) + ) { + return true + } + + // If the projection is just the alias (whole row) without a specific field, + // we can't verify whether the field we're referencing is being preserved or remapped. + if (projected.path.length < 2) { + return true + } + + const [innerAlias, innerField] = projected.path + + // Safe only when the projection points straight back to the same alias or the + // underlying source alias and preserves the field name. + const firstFromAlias = getFirstFromAlias(subquery) + if (innerAlias !== outerAlias && innerAlias !== firstFromAlias) { + return true + } + + if (innerField !== path[1]) { + return true + } + } + + return false +} + +/** A copy of the query whose clause arrays are new arrays. */ +function copyClauseArrays(query: QueryIR): QueryIR { + return { + ...query, + groupBy: query.groupBy ? [...query.groupBy] : undefined, + having: query.having ? [...query.having] : undefined, + orderBy: query.orderBy ? [...query.orderBy] : undefined, + fnWhere: query.fnWhere ? [...query.fnWhere] : undefined, + fnHaving: query.fnHaving ? [...query.fnHaving] : undefined, + } +} + +function deepCopyFrom(from: From): From { + // Share source refs: their SourceId is the compiled input identity. + if (from.type === `collectionRef` || from.type === `descriptorRef`) { + return from + } + + if (from.type === `queryRef`) { + return new QueryRefClass( + deepCopyQuery(from.query), + from.alias, + from.bindingId, + ) + } + + if (from.type === `unionAll`) { + return new UnionAllClass( + from.queries.map((branch) => deepCopyQuery(branch)), + ) + } + + return new UnionFromClass( + from.sources.map( + (source: CollectionSourceRef | QueryRefClass) => + deepCopyFrom(source) as any, + ), + ) +} + diff --git a/packages/db/src/query/optimizer.ts b/packages/db/src/query/optimizer.ts index bb0c272583..f0648b06b7 100644 --- a/packages/db/src/query/optimizer.ts +++ b/packages/db/src/query/optimizer.ts @@ -122,15 +122,11 @@ import { deepEquals } from '../utils.js' import { CannotCombineEmptyExpressionListError } from '../errors.js' -import { containsAggregate } from './compiler/group-by.js' import { Func, - PropRef, QueryRef as QueryRefClass, UnionAll as UnionAllClass, UnionFrom as UnionFromClass, - collectPropRefs, - createResidualWhere, getFromSources, getWhereExpression, isResidualWhere, @@ -140,7 +136,6 @@ import type { CollectionSourceRef, From, QueryIR, - Select, Where, } from './ir.js' @@ -341,7 +336,7 @@ function isCollectionReference(query: QueryIR, sourceAlias: string): boolean { * unmatched outer-join rows (with the nullable side set to undefined) and * incorrectly survive residual filtering. */ -function getNullableJoinSources(query: QueryIR): Set { +export function getNullableJoinSources(query: QueryIR): Set { const nullable = new Set() if (query.join) { const leftAliases = new Set( @@ -369,7 +364,7 @@ function getNullableJoinSources(query: QueryIR): Set { * @param query - The QueryIR to optimize * @returns A new QueryIR with optimizations applied */ -function applyRecursiveOptimization(query: QueryIR): QueryIR { +export function applyRecursiveOptimization(query: QueryIR): QueryIR { // First, recursively optimize any existing subqueries const subqueriesOptimized = { ...query, @@ -443,8 +438,9 @@ function applySingleLevelOptimization(query: QueryIR): QueryIR { // Step 3: Group clauses by single-source vs multi-source const groupedClauses = groupWhereClauses(analyzedClauses) - // Step 4: Apply optimizations by lifting single-source clauses into subqueries - return applyOptimizations(query, groupedClauses) + // Step 4: The join operator supplies the pushdown that lifts single-source + // clauses into subqueries, so apps without joins never import it. + return query.join[0]!.optimize(query, groupedClauses) } /** @@ -574,7 +570,7 @@ function splitAndClauses( } // Helper function for recursive splitting of BasicExpression arrays -function splitAndClausesRecursive( +export function splitAndClausesRecursive( clause: BasicExpression, ): Array> { if (clause.type === `func` && clause.name === `and`) { @@ -614,7 +610,7 @@ function splitAndClausesRecursive( * // eq(special.id, 5) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown) * ``` */ -function getLocalBindingIds(query: QueryIR): Set { +export function getLocalBindingIds(query: QueryIR): Set { return new Set([ ...getFromSources(query.from).map((source) => source.bindingId), ...(query.join?.map(({ from }) => from.bindingId) ?? []), @@ -736,176 +732,6 @@ function groupWhereClauses( } } -/** - * Step 4: Apply optimizations by lifting single-source clauses into subqueries. - * - * Creates a new QueryIR with single-source WHERE clauses moved to subqueries - * that wrap the original table references. This ensures immutability and prevents - * infinite recursion issues. - * - * @param query - Original QueryIR to optimize - * @param groupedClauses - WHERE clauses grouped by optimization strategy - * @returns New QueryIR with optimizations applied - */ -function applyOptimizations( - query: QueryIR, - groupedClauses: GroupedWhereClauses, -): QueryIR { - // Track which single-source clauses were actually optimized - const actuallyOptimized = new Set() - - // Determine which source aliases are on the nullable side of outer joins. - const nullableSources = getNullableJoinSources(query) - - // Build a filtered copy of singleSource that excludes nullable-side clauses. - // Pushing a WHERE clause into the nullable side's subquery pre-filters the - // data before the join, converting "matched but WHERE-excluded" rows into - // "unmatched" outer-join rows. These are indistinguishable from genuinely - // unmatched rows, so the residual WHERE cannot correct the result. - const pushableSingleSource = new Map>() - for (const [source, clause] of groupedClauses.singleSource) { - if (!nullableSources.has(source)) { - pushableSingleSource.set(source, clause) - } - } - - // Optimize the main FROM clause and track what was optimized - const optimizedFrom = optimizeFromWithTracking( - query.from, - pushableSingleSource, - actuallyOptimized, - ) - - // Optimize JOIN clauses and track what was optimized - const optimizedJoins = query.join - ? query.join.map((joinClause) => ({ - ...joinClause, - from: optimizeFromWithTracking( - joinClause.from, - pushableSingleSource, - actuallyOptimized, - ) as CollectionSourceRef | QueryRefClass, - })) - : undefined - - // Keep regular and residual clauses separate so neither loses its marker. - const remainingWhereClauses: Array = [] - const residualWhereClauses = query.where?.filter(isResidualWhere) ?? [] - - // Add multi-source clauses - if (groupedClauses.multiSource) { - remainingWhereClauses.push(groupedClauses.multiSource) - } - - // Determine if we need residual clauses (when query has outer JOINs) - const hasOuterJoins = nullableSources.size > 0 - - // Add single-source clauses - for (const [source, clause] of groupedClauses.singleSource) { - if (!actuallyOptimized.has(source)) { - // Wasn't optimized at all - keep as regular WHERE clause - remainingWhereClauses.push(clause) - } else if (hasOuterJoins) { - // Was optimized AND query has outer JOINs - keep as residual WHERE clause - residualWhereClauses.push(createResidualWhere(clause)) - } - // If optimized and no outer JOINs - don't keep (original behavior) - } - - // Combine within each group to avoid extra filters and nested ANDs. - const combineRemaining = (clauses: Array): Where | undefined => - clauses.length > 1 - ? combineWithAnd( - clauses.flatMap((clause) => - splitAndClausesRecursive(getWhereExpression(clause)), - ), - ) - : clauses[0] - const regularWhere = combineRemaining(remainingWhereClauses) - const residualWhere = combineRemaining(residualWhereClauses) - const finalWhere: Array = [ - ...(regularWhere ? [regularWhere] : []), - ...(residualWhere - ? [createResidualWhere(getWhereExpression(residualWhere))] - : []), - ] - - // Preserve untouched query options while replacing the optimized clauses. - const optimizedQuery: QueryIR = { - ...copyClauseArrays(query), - from: optimizedFrom, - join: optimizedJoins, - where: finalWhere.length > 0 ? finalWhere : [], - } - - return optimizedQuery -} - -/** - * Helper function to create a deep copy of a QueryIR object for immutability. - * - * This ensures that all optimizations create new objects rather than modifying - * existing ones, preventing infinite recursion and shared reference issues. - * - * @param query - QueryIR to deep copy - * @returns New QueryIR object with all nested objects copied - */ -function deepCopyQuery(query: QueryIR): QueryIR { - return { - ...copyClauseArrays(query), - from: deepCopyFrom(query.from), - join: query.join - ? query.join.map((joinClause) => ({ - type: joinClause.type, - on: joinClause.on, - from: deepCopyFrom(joinClause.from) as - CollectionSourceRef | QueryRefClass, - })) - : undefined, - where: query.where ? [...query.where] : undefined, - } -} - -/** A copy of the query whose clause arrays are new arrays. */ -function copyClauseArrays(query: QueryIR): QueryIR { - return { - ...query, - groupBy: query.groupBy ? [...query.groupBy] : undefined, - having: query.having ? [...query.having] : undefined, - orderBy: query.orderBy ? [...query.orderBy] : undefined, - fnWhere: query.fnWhere ? [...query.fnWhere] : undefined, - fnHaving: query.fnHaving ? [...query.fnHaving] : undefined, - } -} - -function deepCopyFrom(from: From): From { - // Share source refs: their SourceId is the compiled input identity. - if (from.type === `collectionRef` || from.type === `descriptorRef`) { - return from - } - - if (from.type === `queryRef`) { - return new QueryRefClass( - deepCopyQuery(from.query), - from.alias, - from.bindingId, - ) - } - - if (from.type === `unionAll`) { - return new UnionAllClass( - from.queries.map((branch) => deepCopyQuery(branch)), - ) - } - - return new UnionFromClass( - from.sources.map( - (source: CollectionSourceRef | QueryRefClass) => - deepCopyFrom(source) as any, - ), - ) -} - function optimizeNestedFrom(from: From): From { if (from.type === `queryRef`) { return new QueryRefClass( @@ -930,304 +756,10 @@ function optimizeNestedFrom(from: From): From { return from } -function getFirstFromAlias(query: QueryIR): string | undefined { +export function getFirstFromAlias(query: QueryIR): string | undefined { return getFromSources(query.from)[0]?.alias } -/** - * Helper function to optimize a FROM clause while tracking what was actually optimized. - * - * @param from - FROM clause to optimize - * @param singleSourceClauses - Map of source aliases to their WHERE clauses - * @param actuallyOptimized - Set to track which sources were actually optimized - * @returns New FROM clause, potentially wrapped in a subquery - */ -function optimizeFromWithTracking( - from: From, - singleSourceClauses: Map>, - actuallyOptimized: Set, -): From { - if (from.type === `unionFrom`) { - return new UnionFromClass( - from.sources.map( - (source) => - optimizeFromWithTracking( - source, - singleSourceClauses, - actuallyOptimized, - ) as CollectionSourceRef | QueryRefClass, - ), - ) - } - - if (from.type === `unionAll`) { - return new UnionAllClass( - from.queries.map((branch) => deepCopyQuery(branch)), - ) - } - - const whereClause = singleSourceClauses.get(from.alias) - - if (!whereClause) { - // No optimization needed. Keep the source ref itself: its sourceId is - // the compiled input identity, and a copy would lose it. - if (from.type === `collectionRef` || from.type === `descriptorRef`) { - return from - } - // Must be queryRef due to type system - return new QueryRefClass( - deepCopyQuery(from.query), - from.alias, - from.bindingId, - ) - } - - if (from.type === `collectionRef` || from.type === `descriptorRef`) { - // Create a new subquery with the WHERE clause for the collection - // This is always safe since we're creating a new subquery - const subQuery: QueryIR = { - from, - where: [whereClause], - } - actuallyOptimized.add(from.alias) // Mark as successfully optimized - return new QueryRefClass(subQuery, from.alias, from.bindingId) - } - - // SAFETY CHECK: Only check safety when pushing WHERE clauses into existing subqueries - // We need to be careful about pushing WHERE clauses into subqueries that already have - // aggregates, HAVING, or ORDER BY + LIMIT since that could change their semantics - if (!isSafeToPushIntoExistingSubquery(from.query, whereClause, from.alias)) { - // Return a copy without optimization to maintain immutability - // Do NOT mark as optimized since we didn't actually optimize it - return new QueryRefClass( - deepCopyQuery(from.query), - from.alias, - from.bindingId, - ) - } - - // Skip pushdown when a clause references a field that only exists via a renamed - // projection inside the subquery; leaving it outside preserves the alias mapping. - if (referencesAliasWithRemappedSelect(from.query, whereClause, from.alias)) { - return new QueryRefClass( - deepCopyQuery(from.query), - from.alias, - from.bindingId, - ) - } - - // Add the WHERE clause to the existing subquery - // Create a deep copy to ensure immutability - const existingWhere = from.query.where || [] - const remappedWhere = remapWhereForSubquery( - from.query, - whereClause, - from.alias, - ) - if (remappedWhere === undefined) { - return new QueryRefClass( - deepCopyQuery(from.query), - from.alias, - from.bindingId, - ) - } - const optimizedSubQuery: QueryIR = { - ...deepCopyQuery(from.query), - where: [...existingWhere, remappedWhere], - } - actuallyOptimized.add(from.alias) // Mark as successfully optimized - return new QueryRefClass(optimizedSubQuery, from.alias, from.bindingId) -} - -/** - * Rewrites references to an outer QueryRef alias so a pushed predicate can be - * evaluated inside the subquery's namespace. Pass-through SELECT fields use - * their projected source path; unprojected rows use the first source alias. - */ -function remapWhereForSubquery( - subquery: QueryIR, - whereClause: BasicExpression, - outerAlias: string, -): BasicExpression | undefined { - const firstFromAlias = getFirstFromAlias(subquery) - if (firstFromAlias === undefined) return undefined - - const remapExpression = (expression: BasicExpression): BasicExpression => { - if (expression instanceof PropRef) { - if (expression.path[0] !== outerAlias) return expression - - const field = expression.path[1] - const projected = field ? subquery.select?.[field] : undefined - const hasNamespacedResult = - subquery.join !== undefined || subquery.from.type === `unionFrom` - const innerPath = - projected instanceof PropRef - ? [...projected.path, ...expression.path.slice(2)] - : hasNamespacedResult - ? expression.path.slice(1) - : [firstFromAlias, ...expression.path.slice(1)] - - return new PropRef(innerPath) - } - - if (expression instanceof Func) { - return new Func(expression.name, expression.args.map(remapExpression)) - } - - return expression - } - - return remapExpression(whereClause) as BasicExpression -} - -function isSafeToPushIntoExistingSubquery( - query: QueryIR, - whereClause: BasicExpression, - outerAlias: string, -): boolean { - return !( - query.distinct || - (query.select && - (containsAggregate(query.select) || - whereReferencesComputedSelectFields( - query.select, - whereClause, - outerAlias, - ))) || - query.groupBy?.length || - query.having?.length || - (query.orderBy?.length && - (query.limit !== undefined || query.offset !== undefined)) || - query.fnSelect || - query.fnWhere?.length || - query.fnHaving?.length - ) -} - -/** - * Determines whether the provided WHERE clause references fields that are - * computed by a subquery SELECT rather than pass-through properties. - * - * If true, pushing the WHERE clause into the subquery could change semantics - * (since computed fields do not necessarily exist at the subquery input level), - * so predicate pushdown must be avoided. - * - * @param select - The subquery SELECT map - * @param whereClause - The WHERE expression to analyze - * @param outerAlias - The alias of the subquery in the outer query - * @returns True if WHERE references computed fields, otherwise false - */ -function whereReferencesComputedSelectFields( - select: Select, - whereClause: BasicExpression, - outerAlias: string, -): boolean { - // Build a set of computed field names at the top-level of the subquery output - const computed = new Set() - for (const [key, value] of Object.entries(select)) { - if (key.startsWith(`__SPREAD_SENTINEL__`)) continue - if (value instanceof PropRef) continue - // Nested object or non-PropRef expression counts as computed - computed.add(key) - } - - const refs = collectPropRefs(whereClause) - - for (const ref of refs) { - const path = (ref as any).path as Array - if (!Array.isArray(path) || path.length < 2) continue - const alias = path[0] - const field = path[1] as string - if (alias !== outerAlias) continue - if (computed.has(field)) return true - } - return false -} - -/** - * Detects whether a WHERE clause references the subquery alias through fields that - * are re-exposed under different names (renamed SELECT projections or fnSelect output). - * In those cases we keep the clause at the outer level to avoid alias remapping bugs. - * TODO: in future we should handle this by rewriting the clause to use the subquery's - * internal field references, but it likely needs a wider refactor to do cleanly. - */ -function referencesAliasWithRemappedSelect( - subquery: QueryIR, - whereClause: BasicExpression, - outerAlias: string, -): boolean { - const refs = collectPropRefs(whereClause) - // Only care about clauses that actually reference the outer alias. - if (refs.every((ref) => ref.path[0] !== outerAlias)) { - return false - } - - // fnSelect always rewrites the row shape, so alias-safe pushdown is impossible. - if (subquery.fnSelect) { - return true - } - - const select = subquery.select - // Without an explicit SELECT the clause still refers to the original collection. - if (!select) { - return false - } - const hasSpreadProjection = Object.keys(select).some((key) => - key.startsWith(`__SPREAD_SENTINEL__`), - ) - const localBindings = getLocalBindingIds(subquery) - - for (const ref of refs) { - const path = ref.path - // Need at least alias + field to matter. - if (path.length < 2) continue - if (path[0] !== outerAlias) continue - - const projected = select[path[1]!] - // A spread-selected field has no direct projection entry to remap. - // Keep its predicate outside rather than guessing its input source. - if (!projected) { - if (hasSpreadProjection) return true - continue - } - - // Non-PropRef projections are computed values; cannot push down. - if (!(projected instanceof PropRef)) { - return true - } - - // A selected ancestor field depends on the parent route. Moving its - // predicate into this source would read a same-named child field instead. - if ( - projected.bindingId !== undefined && - !localBindings.has(projected.bindingId) - ) { - return true - } - - // If the projection is just the alias (whole row) without a specific field, - // we can't verify whether the field we're referencing is being preserved or remapped. - if (projected.path.length < 2) { - return true - } - - const [innerAlias, innerField] = projected.path - - // Safe only when the projection points straight back to the same alias or the - // underlying source alias and preserves the field name. - const firstFromAlias = getFirstFromAlias(subquery) - if (innerAlias !== outerAlias && innerAlias !== firstFromAlias) { - return true - } - - if (innerField !== path[1]) { - return true - } - } - - return false -} - /** * Helper function to combine multiple expressions with AND. * @@ -1238,7 +770,7 @@ function referencesAliasWithRemappedSelect( * @returns Single expression representing the AND combination * @throws Error if the expressions array is empty */ -function combineWithAnd( +export function combineWithAnd( expressions: Array>, ): BasicExpression { if (expressions.length === 0) { diff --git a/packages/db/tests/collection-auto-index.test.ts b/packages/db/tests/collection-auto-index.test.ts index 7d004d4946..39aa43b0fa 100644 --- a/packages/db/tests/collection-auto-index.test.ts +++ b/packages/db/tests/collection-auto-index.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from 'vitest' +import { createLiveQueryCollection, leftJoin } from '../src' import { CollectionConfigurationError } from '../src/errors' import { createCollection } from '../src/collection/index.js' import { @@ -11,7 +12,6 @@ import { or, } from '../src/query/builder/functions' import { createSingleRowRefProxy } from '../src/query/builder/ref-proxy' -import { createLiveQueryCollection } from '../src' import { PropRef } from '../src/query/ir' import { BTreeIndex } from '../src/indexes/btree-index' import { @@ -582,11 +582,8 @@ describe(`Collection Auto-Indexing`, () => { query: (q: any) => q .from({ item: leftCollection }) - .join( - { other: rightCollection }, - ({ item, other }: any) => eq(item.id, other.id2), - `left`, - ) + .join(leftJoin({ other: rightCollection }, + ({ item, other }: any) => eq(item.id, other.id2))) .select(({ item, other }: any) => ({ id: item.id, name: item.name, @@ -696,8 +693,7 @@ describe(`Collection Auto-Indexing`, () => { query: (q: any) => q .from({ item: leftCollection }) - .join( - { + .join(leftJoin({ other: q .from({ other: rightCollection }) .select(({ other }: any) => ({ @@ -705,9 +701,7 @@ describe(`Collection Auto-Indexing`, () => { name: other.name, })), }, - ({ item, other }: any) => eq(item.id, other.id2), - `left`, - ) + ({ item, other }: any) => eq(item.id, other.id2))) .select(({ item, other }: any) => ({ id: item.id, name: item.name, diff --git a/packages/db/tests/collection-cleanup-restart-oracle.test.ts b/packages/db/tests/collection-cleanup-restart-oracle.test.ts index 356f91cbf0..93a115977f 100644 --- a/packages/db/tests/collection-cleanup-restart-oracle.test.ts +++ b/packages/db/tests/collection-cleanup-restart-oracle.test.ts @@ -1,12 +1,11 @@ import { describe, expect, it, vi } from 'vitest' -import { - BasicIndex, +import { BasicIndex, createCollection, createEffect, createLiveQueryCollection, eq, - withCollectionSyncConfigCleanup, -} from '../src' + innerJoin, + withCollectionSyncConfigCleanup } from '../src' import { createDeferred } from '../src/deferred' import type { SyncConfig } from '../src/types' @@ -520,11 +519,8 @@ describe(`Collection cleanup admission oracle`, () => { const live = createLiveQueryCollection((q) => q .from({ left: source }) - .join( - { right: source }, - ({ left, right }) => eq(left.id, right.id), - `inner`, - ), + .join(innerJoin({ right: source }, + ({ left, right }) => eq(left.id, right.id))), ) const reports = vi.spyOn(console, `error`).mockImplementation(() => {}) @@ -882,11 +878,8 @@ describe(`Collection cleanup admission oracle`, () => { live = createLiveQueryCollection((q) => q .from({ first: firstSource }) - .join( - { second: secondSource }, - ({ first, second }) => eq(first.id, second.id), - `inner`, - ), + .join(innerJoin({ second: secondSource }, + ({ first, second }) => eq(first.id, second.id))), ) const message = `Source collection '${cleaningSource.id}' was manually cleaned up while live query '${live.id}' depends on it. ` + diff --git a/packages/db/tests/collection-query-publication-boundaries.test.ts b/packages/db/tests/collection-query-publication-boundaries.test.ts index f5197fc4e1..f988752d2f 100644 --- a/packages/db/tests/collection-query-publication-boundaries.test.ts +++ b/packages/db/tests/collection-query-publication-boundaries.test.ts @@ -1,8 +1,8 @@ import { expect, it } from 'vitest' import { MultiSet } from '@tanstack/db-ivm' +import { createLiveQueryCollection, eq, innerJoin } from '../src/query/index.js' import { createCollection } from '../src/collection/index.js' import { createDeferred } from '../src/deferred.js' -import { createLiveQueryCollection, eq } from '../src/query/index.js' import { createTransaction } from '../src/transactions.js' import { flushPromises } from './utils.js' import type { SyncConfig } from '../src/types.js' @@ -67,7 +67,7 @@ it.each([0, 2])(`opens an inner-join window from limit %s`, async (limit) => { query: (q) => q .from({ a: parent }) - .join({ b: child }, ({ a, b }) => eq(a.id, b.id), `inner`) + .join(innerJoin({ b: child }, ({ a, b }) => eq(a.id, b.id))) .orderBy(({ a }) => a.value) .limit(limit) .select(({ a }) => ({ id: a.id, value: a.value })), diff --git a/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts b/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts index c91a976831..18ede5b64a 100644 --- a/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts +++ b/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' +import { createLiveQueryCollection, eq, leftJoin } from '../src/query/index.js' import { createCollection } from '../src/collection/index.js' import { BTreeIndex } from '../src/indexes/btree-index.js' -import { createLiveQueryCollection, eq } from '../src/query/index.js' import { mockSyncCollectionOptions } from './utils.js' import type { ChangeMessage } from '../src/types.js' @@ -505,9 +505,8 @@ describe(`CollectionSubscriber duplicate insert prevention`, () => { const liveQueryCollection = createLiveQueryCollection((q) => q .from({ users: usersCollection }) - .join({ orders: ordersCollection }, ({ users: u, orders: o }) => - eq(u.id, o.userId), - ) + .join(leftJoin({ orders: ordersCollection }, ({ users: u, orders: o }) => + eq(u.id, o.userId))) .select(({ users: u2, orders: o2 }) => ({ orderId: o2.id, userName: u2.name, diff --git a/packages/db/tests/conformance/suite.ts b/packages/db/tests/conformance/suite.ts index 6882c980ee..f09ca205fc 100644 --- a/packages/db/tests/conformance/suite.ts +++ b/packages/db/tests/conformance/suite.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest' /** * Shared live-query conformance suite. * @@ -36,8 +37,7 @@ * no-data, initial readiness over a lagging on-demand source, param recompilation, optimistic mutation, pre-created & config-object * inputs, error status, and order-only moves. */ -import { describe, expect, it } from 'vitest' -import { waitForPersistedInitialRender } from '@tanstack/db' +import { leftJoin, waitForPersistedInitialRender } from '@tanstack/db' import { ScenarioLifetime } from './scenario-lifetime' import { ScenarioSources } from './scenario-sources' import { scenarioRegistry } from './registration' @@ -206,9 +206,8 @@ export function runSuite(rawDriver: LiveQueryDriver) { const h = driver.mount((q) => q .from({ left: left.collection }) - .join({ right: right.collection }, ({ left: a, right: b }: any) => - ops.eq(a.id, b.id), - ), + .join(leftJoin({ right: right.collection }, ({ left: a, right: b }: any) => + ops.eq(a.id, b.id))), ) await h.flush() expect(h.current()).toMatchObject({ @@ -247,10 +246,8 @@ export function runSuite(rawDriver: LiveQueryDriver) { const h = driver.mount((q) => q .from({ opted: opted.collection }) - .join( - { ordinary: ordinary.collection }, - ({ opted: a, ordinary: b }: any) => ops.eq(a.id, b.id), - ), + .join(leftJoin({ ordinary: ordinary.collection }, + ({ opted: a, ordinary: b }: any) => ops.eq(a.id, b.id))), ) await h.apply(settle) await h.flush() @@ -637,9 +634,8 @@ export function runSuite(rawDriver: LiveQueryDriver) { const h = driver.mount((q) => q .from({ issues: issues.collection }) - .join({ persons: people.collection }, ({ issues: i, persons }: any) => - ops.eq(i.userId, persons.id), - ) + .join(leftJoin({ persons: people.collection }, ({ issues: i, persons }: any) => + ops.eq(i.userId, persons.id))) .select(({ issues: i, persons }: any) => ({ id: i.id, title: i.title, @@ -702,9 +698,8 @@ export function runSuite(rawDriver: LiveQueryDriver) { })) return q .from({ persons: people.collection }) - .leftJoin({ ic: issueCounts }, ({ persons, ic }: any) => - ops.eq(persons.id, ic.userId), - ) + .join(leftJoin({ ic: issueCounts }, ({ persons, ic }: any) => + ops.eq(persons.id, ic.userId))) .select(({ persons, ic }: any) => ({ name: persons.name, issueCount: ic.issueCount, diff --git a/packages/db/tests/effect.test.ts b/packages/db/tests/effect.test.ts index 81f7b61e51..4bd2f08be4 100644 --- a/packages/db/tests/effect.test.ts +++ b/packages/db/tests/effect.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' +import { Query, createEffect, createTransaction, eq, leftJoin } from '../src/index.js' import { createCollection } from '../src/collection/index.js' import { BTreeIndex } from '../src/indexes/btree-index.js' -import { Query, createEffect, createTransaction, eq } from '../src/index.js' import { captureMicrotaskErrors, mockSyncCollectionOptions, @@ -919,9 +919,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ issue: issues }) - .join({ user: users }, ({ issue, user }) => - eq(issue.userId, user.id), - ) + .join(leftJoin({ user: users }, ({ issue, user }) => + eq(issue.userId, user.id))) .select(({ issue, user }) => ({ issueId: issue.id, title: issue.title, @@ -1666,9 +1665,8 @@ describe(`createEffect`, () => { return q .from({ left: leftTop }) - .join({ right: rightTop }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.id, rightRow.id), - ) + .join(leftJoin({ right: rightTop }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.id, rightRow.id))) .select(({ left: leftRow, right: rightRow }) => ({ id: leftRow.id, leftName: leftRow.name, @@ -1722,9 +1720,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ) + .join(leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId))) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -1765,9 +1762,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ left }) - .leftJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.id, rightRow.id), - ), + .join(leftJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.id, rightRow.id))), onBatch: () => {}, }) @@ -1828,9 +1824,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ left }) - .leftJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.id, rightRow.id), - ), + .join(leftJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.id, rightRow.id))), onBatch: () => {}, }), ).toThrow(startupFailure) @@ -1939,9 +1934,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ) + .join(leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId))) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -1986,9 +1980,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ) + .join(leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId))) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2057,9 +2050,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ) + .join(leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId))) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2227,9 +2219,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ) + .join(leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId))) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2284,9 +2275,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ) + .join(leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId))) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2485,9 +2475,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ) + .join(leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId))) .fn.select(({ user }) => { if (phase === `projection`) disposeInTurn?.() return user diff --git a/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts b/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts index 6ba058ed65..acd25c8e34 100644 --- a/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts +++ b/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts @@ -1,3 +1,4 @@ +import { describe, expect, it, vi } from 'vitest' /** * # When may a live-query Collection start provider work? * @@ -45,7 +46,6 @@ * `DbClient` stream preloads, a subscriber that outlives cleanup, and reads * that wait for readiness. */ -import { describe, expect, it, vi } from 'vitest' import { BTreeIndex, DbClient, @@ -55,6 +55,7 @@ import { createLiveQueryObserver, eq, getLiveQueryHash, + leftJoin, prepareLiveQueryValue, } from '../src' import { Query } from '../src/query/builder/index.js' @@ -1222,7 +1223,7 @@ describe(`a source whose start throws on resumption`, () => { query: (q) => q .from({ a: failing }) - .join({ b: healthy }, ({ a, b }) => eq(a.id, b.id)), + .join(leftJoin({ b: healthy }, ({ a, b }) => eq(a.id, b.id))), gcTime: 0, }) const consoleError = vi.spyOn(console, `error`).mockImplementation(() => {}) diff --git a/packages/db/tests/live-query-options.test.ts b/packages/db/tests/live-query-options.test.ts index b138edf23e..9e3971f0fa 100644 --- a/packages/db/tests/live-query-options.test.ts +++ b/packages/db/tests/live-query-options.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { leftJoin } from '@tanstack/db' import { createCollection } from '../src/collection/index.js' import { DbClient, collectionOptions } from '../src/client.js' import { @@ -125,10 +126,8 @@ describe(`live query preparation`, () => { for (const collection of renderDeferrals) collection._resumeSyncStart() renderDeferrals.clear() - const continued = prepared.join( - { later: laterDescriptor }, - ({ source, later }) => eq(source.id, later.id), - ) + const continued = prepared.join(leftJoin({ later: laterDescriptor }, + ({ source, later }) => eq(source.id, later.id))) expect( collectSourceRefs(getQueryIR(continued)).map((ref) => ref.type), ).toEqual([`collectionRef`, `descriptorRef`]) @@ -169,10 +168,8 @@ describe(`live query preparation`, () => { client, deferrals, ) as typeof base - const continued = prepared.join( - { later: laterDescriptor }, - ({ source, later }) => eq(source.id, later.id), - ) + const continued = prepared.join(leftJoin({ later: laterDescriptor }, + ({ source, later }) => eq(source.id, later.id))) expect(starts).toBe(0) const consumed = prepareLiveQueryValue( @@ -215,10 +212,8 @@ describe(`live query preparation`, () => { new Set(), ) - const continued = captured!.join( - { later: laterDescriptor }, - ({ source, later }) => eq(source.id, later.id), - ) + const continued = captured!.join(leftJoin({ later: laterDescriptor }, + ({ source, later }) => eq(source.id, later.id))) expect( collectSourceRefs(getQueryIR(continued)).map((ref) => ref.type), ).toEqual([`descriptorRef`, `descriptorRef`]) diff --git a/packages/db/tests/query/aggregate-value-contracts.test-d.ts b/packages/db/tests/query/aggregate-value-contracts.test-d.ts index 88030a3977..fa303970c0 100644 --- a/packages/db/tests/query/aggregate-value-contracts.test-d.ts +++ b/packages/db/tests/query/aggregate-value-contracts.test-d.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, test } from 'vitest' +import { createLiveQueryCollection, leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' -import { createLiveQueryCollection } from '../../src/query/index.js' import { add, avg, @@ -244,9 +244,8 @@ describe(`aggregate value contracts`, () => { query: (q) => q .from({ row: rows }) - .leftJoin({ detail: details }, ({ row, detail }) => - eq(row.id, detail.rowId), - ) + .join(leftJoin({ detail: details }, ({ row, detail }) => + eq(row.id, detail.rowId))) .groupBy(({ row }) => row.group) .select(({ row, detail }) => { expectTypeOf(sum(detail.amount)).toEqualTypeOf>() diff --git a/packages/db/tests/query/async-hold-outcomes.test.ts b/packages/db/tests/query/async-hold-outcomes.test.ts index e302691aa2..11ea94cf21 100644 --- a/packages/db/tests/query/async-hold-outcomes.test.ts +++ b/packages/db/tests/query/async-hold-outcomes.test.ts @@ -1,8 +1,8 @@ import { afterEach, describe, expect, it } from 'vitest' +import { createLiveQueryCollection, eq, leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { createEffect } from '../../src/query/effect.js' -import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { LoadSubsetOptions } from '../../src/types.js' @@ -160,9 +160,8 @@ describe(`live-query lazy demand hold outcomes`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ user: users }) - .leftJoin({ post: posts.collection }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: posts.collection }, ({ user, post }) => + eq(user.id, post.userId))) .select(({ user, post }) => ({ id: user.id, postId: post.id })), ) const subscription = liveQuery.subscribeChanges(() => {}) @@ -190,9 +189,8 @@ describe(`live-query lazy demand hold outcomes`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ user: users }) - .leftJoin({ post: posts.collection }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: posts.collection }, ({ user, post }) => + eq(user.id, post.userId))) .select(({ user, post }) => ({ id: user.id, postId: post.id })), ) cleanups.push(() => liveQuery.cleanup()) diff --git a/packages/db/tests/query/builder/buildQuery.test.ts b/packages/db/tests/query/builder/buildQuery.test.ts index 292d6f892b..1f13e41742 100644 --- a/packages/db/tests/query/builder/buildQuery.test.ts +++ b/packages/db/tests/query/builder/buildQuery.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { leftJoin } from '@tanstack/db' import { CollectionImpl } from '../../../src/collection/index.js' import { buildQuery } from '../../../src/query/builder/index.js' import { and, eq, gt, or } from '../../../src/query/builder/functions.js' @@ -60,11 +61,9 @@ describe(`buildQuery function`, () => { const query = buildQuery((q) => q .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .select(({ employees, departments }) => ({ employee_name: employees.name, department_name: departments.name, @@ -121,9 +120,8 @@ describe(`buildQuery function`, () => { const query = buildQuery((q) => q .from({ comment: commentsCollection }) - .join({ user: usersCollection }, ({ comment, user }) => - eq(comment.user_id, user.id), - ) + .join(leftJoin({ user: usersCollection }, ({ comment, user }) => + eq(comment.user_id, user.id))) .where(({ comment }) => or(eq(comment.id, 1), eq(comment.id, 2))) .orderBy(({ comment }) => comment.date, `desc`) .select(({ comment, user }) => ({ diff --git a/packages/db/tests/query/builder/callback-types.test-d.ts b/packages/db/tests/query/builder/callback-types.test-d.ts index a26972b290..45276047eb 100644 --- a/packages/db/tests/query/builder/callback-types.test-d.ts +++ b/packages/db/tests/query/builder/callback-types.test-d.ts @@ -1,4 +1,5 @@ import { describe, expectTypeOf, test } from 'vitest' +import { leftJoin } from '@tanstack/db' import { createCollection } from '../../../src/collection/index.js' import { mockSyncCollectionOptions } from '../../utils.js' import { Query } from '../../../src/query/builder/index.js' @@ -114,9 +115,8 @@ describe(`Query Builder Callback Types`, () => { test(`refProxy with joins in select callback`, () => { new Query() .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => { // Test cross-table property access expectTypeOf( @@ -268,9 +268,8 @@ describe(`Query Builder Callback Types`, () => { test(`refProxy with joins in where callback`, () => { new Query() .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .where(({ user, dept }) => { expectTypeOf(user.active).toEqualTypeOf>() expectTypeOf(dept.active).toEqualTypeOf>() @@ -289,7 +288,7 @@ describe(`Query Builder Callback Types`, () => { test(`refProxy types in join on callback`, () => { new Query() .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => { + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { // Test property access for join conditions expectTypeOf( user.department_id, @@ -297,29 +296,28 @@ describe(`Query Builder Callback Types`, () => { expectTypeOf(dept.id).toEqualTypeOf>() return eq(user.department_id, dept.id) - }) + })) }) test(`complex join conditions`, () => { new Query() .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => { + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { // Test complex join conditions with multiple operators expectTypeOf( and(eq(user.department_id, dept.id), eq(dept.active, true)), ).toEqualTypeOf>() return and(eq(user.department_id, dept.id), eq(dept.active, true)) - }) + })) }) test(`multiple joins with correct context`, () => { new Query() .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) - .join({ project: projectsCollection }, ({ user, dept, project }) => { + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) + .join(leftJoin({ project: projectsCollection }, ({ user, dept, project }) => { expectTypeOf(user.id).toEqualTypeOf>() expectTypeOf(dept.id).toEqualTypeOf>() expectTypeOf(project.user_id).toEqualTypeOf>() @@ -329,7 +327,7 @@ describe(`Query Builder Callback Types`, () => { eq(project.user_id, user.id), eq(project.department_id, dept.id), ) - }) + })) }) }) @@ -361,9 +359,8 @@ describe(`Query Builder Callback Types`, () => { test(`orderBy with joins`, () => { new Query() .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .orderBy(({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() expectTypeOf(dept.id).toEqualTypeOf>() @@ -401,9 +398,8 @@ describe(`Query Builder Callback Types`, () => { test(`groupBy with joins`, () => { new Query() .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .groupBy(({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() expectTypeOf(dept.id).toEqualTypeOf>() @@ -482,9 +478,8 @@ describe(`Query Builder Callback Types`, () => { test(`having with joins`, () => { new Query() .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .groupBy(({ dept }) => dept.location) .having(({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() @@ -500,20 +495,20 @@ describe(`Query Builder Callback Types`, () => { test(`complex query with all callback types`, () => { new Query() .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => { + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() expectTypeOf(dept.id).toEqualTypeOf>() expectTypeOf(dept.location).toEqualTypeOf>() return eq(user.department_id, dept.id) - }) - .join({ project: projectsCollection }, ({ user, dept, project }) => { + })) + .join(leftJoin({ project: projectsCollection }, ({ user, dept, project }) => { expectTypeOf(user.id).toEqualTypeOf>() expectTypeOf(dept.id).toEqualTypeOf>() expectTypeOf(dept.location).toEqualTypeOf>() expectTypeOf(project.user_id).toEqualTypeOf>() expectTypeOf(project.department_id).toEqualTypeOf>() return eq(project.user_id, user.id) - }) + })) .where(({ user, dept, project }) => { expectTypeOf(user.id).toEqualTypeOf>() expectTypeOf(dept.id).toEqualTypeOf>() diff --git a/packages/db/tests/query/builder/from.test.ts b/packages/db/tests/query/builder/from.test.ts index 17270cdb0e..362e9205ad 100644 --- a/packages/db/tests/query/builder/from.test.ts +++ b/packages/db/tests/query/builder/from.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { leftJoin } from '@tanstack/db' import { CollectionImpl } from '../../../src/collection/index.js' import { Query, getQueryIR } from '../../../src/query/builder/index.js' import { eq } from '../../../src/query/builder/functions.js' @@ -157,7 +158,7 @@ describe(`QueryBuilder.from`, () => { const builder = new Query().from({ employees: employeesCollection }) expect(() => { - builder.join(`departments` as any, () => eq(1, 1)) + builder.join(leftJoin(`departments` as any, () => eq(1, 1))) }).toThrow( /Invalid source for join clause: Expected an object with a single key-value pair.*For example: \.join/, ) diff --git a/packages/db/tests/query/builder/functional-variants.test.ts b/packages/db/tests/query/builder/functional-variants.test.ts index b993d77b9d..77a67e8772 100644 --- a/packages/db/tests/query/builder/functional-variants.test.ts +++ b/packages/db/tests/query/builder/functional-variants.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { leftJoin } from '@tanstack/db' import { CollectionImpl } from '../../../src/collection/index.js' import { Query, getQueryIR } from '../../../src/query/builder/index.js' import { eq, gt } from '../../../src/query/builder/functions.js' @@ -72,11 +73,9 @@ describe(`QueryBuilder functional variants (fn)`, () => { it(`works with joins`, () => { const query = new Query() .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .fn.select((row) => ({ employeeName: row.employees.name, departmentName: row.departments?.name || `No Department`, @@ -142,11 +141,9 @@ describe(`QueryBuilder functional variants (fn)`, () => { it(`works with joins`, () => { const query = new Query() .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .fn.where( (row) => row.employees.active && @@ -216,11 +213,9 @@ describe(`QueryBuilder functional variants (fn)`, () => { it(`works with joins and grouping`, () => { const query = new Query() .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .groupBy(({ departments }) => departments.name) .fn.having( (row) => @@ -238,11 +233,9 @@ describe(`QueryBuilder functional variants (fn)`, () => { it(`supports all functional variants together`, () => { const query = new Query() .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .fn.where((row) => row.employees.active) .fn.where((row) => row.employees.salary > 40000) .groupBy(({ departments }) => departments.name) diff --git a/packages/db/tests/query/builder/join.test.ts b/packages/db/tests/query/builder/join.test.ts index f76d8ad443..25a06dea65 100644 --- a/packages/db/tests/query/builder/join.test.ts +++ b/packages/db/tests/query/builder/join.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { fullJoin, innerJoin, leftJoin, rightJoin } from '@tanstack/db' import { CollectionImpl } from '../../../src/collection/index.js' import { Query, getQueryIR } from '../../../src/query/builder/index.js' import { and, eq, gt } from '../../../src/query/builder/functions.js' @@ -36,11 +37,9 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) const builtQuery = getQueryIR(query) expect(builtQuery.join).toBeDefined() @@ -69,14 +68,11 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) - .join({ projects: projectsCollection }, ({ departments, projects }) => - eq(departments.id, projects.department_id), - ) + eq(employees.department_id, departments.id))) + .join(leftJoin({ projects: projectsCollection }, ({ departments, projects }) => + eq(departments.id, projects.department_id))) const builtQuery = getQueryIR(query) expect(builtQuery.join).toBeDefined() @@ -93,11 +89,9 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .select(({ employees, departments }) => ({ id: employees.id, name: employees.name, @@ -117,11 +111,9 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .where(({ departments }) => gt(departments.budget, 1000000)) const builtQuery = getQueryIR(query) @@ -137,9 +129,8 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join({ bigDepts: subQuery as any }, ({ employees, bigDepts }) => - eq(employees.department_id, (bigDepts as any).id), - ) + .join(leftJoin({ bigDepts: subQuery as any }, ({ employees, bigDepts }) => + eq(employees.department_id, (bigDepts as any).id))) const builtQuery = getQueryIR(query) expect(builtQuery.join).toBeDefined() @@ -154,11 +145,9 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .where(({ employees, departments }) => and(gt(employees.salary, 50000), gt(departments.budget, 1000000)), ) @@ -193,14 +182,11 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) - .join({ users: usersCollection }, ({ employees, users }) => - eq(employees.id, users.employee_id), - ) + eq(employees.department_id, departments.id))) + .join(leftJoin({ users: usersCollection }, ({ employees, users }) => + eq(employees.id, users.employee_id))) const builtQuery = getQueryIR(query) expect(builtQuery.join).toBeDefined() @@ -217,11 +203,9 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .select(({ employees, departments }) => ({ employee: employees, department: departments, @@ -238,20 +222,15 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const explicitQuery = builder .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - `left`, - ) + eq(employees.department_id, departments.id))) const aliasQuery = builder .from({ employees: employeesCollection }) - .leftJoin( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) const explicitQueryIR = getQueryIR(explicitQuery) const aliasQueryIR = getQueryIR(aliasQuery) @@ -264,20 +243,15 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const explicitQuery = builder .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(rightJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - `right`, - ) + eq(employees.department_id, departments.id))) const aliasQuery = builder .from({ employees: employeesCollection }) - .rightJoin( - { departments: departmentsCollection }, + .join(rightJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) const explicitQueryIR = getQueryIR(explicitQuery) const aliasQueryIR = getQueryIR(aliasQuery) @@ -290,20 +264,15 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const explicitQuery = builder .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(innerJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - `inner`, - ) + eq(employees.department_id, departments.id))) const aliasQuery = builder .from({ employees: employeesCollection }) - .innerJoin( - { departments: departmentsCollection }, + .join(innerJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) const explicitQueryIR = getQueryIR(explicitQuery) const aliasQueryIR = getQueryIR(aliasQuery) @@ -316,20 +285,15 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const explicitQuery = builder .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(fullJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - `full`, - ) + eq(employees.department_id, departments.id))) const aliasQuery = builder .from({ employees: employeesCollection }) - .fullJoin( - { departments: departmentsCollection }, + .join(fullJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) const explicitQueryIR = getQueryIR(explicitQuery) const aliasQueryIR = getQueryIR(aliasQuery) @@ -352,16 +316,12 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .leftJoin( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) - .innerJoin( - { projects: projectsCollection }, + eq(employees.department_id, departments.id))) + .join(innerJoin({ projects: projectsCollection }, ({ departments, projects }) => - eq(departments.id, projects.department_id), - ) + eq(departments.id, projects.department_id))) const builtQuery = getQueryIR(query) expect(builtQuery.join).toBeDefined() @@ -380,11 +340,9 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .innerJoin( - { departments: departmentsCollection }, + .join(innerJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .where(({ departments }) => gt(departments.budget, 1000000)) .select(({ employees, departments }) => ({ id: employees.id, diff --git a/packages/db/tests/query/builder/subqueries.test-d.ts b/packages/db/tests/query/builder/subqueries.test-d.ts index 3b9358d0a5..27566101cd 100644 --- a/packages/db/tests/query/builder/subqueries.test-d.ts +++ b/packages/db/tests/query/builder/subqueries.test-d.ts @@ -1,4 +1,5 @@ import { describe, expectTypeOf, test } from 'vitest' +import { leftJoin } from '@tanstack/db' import { Query } from '../../../src/query/builder/index.js' import { CollectionImpl } from '../../../src/collection/index.js' import { avg, count, eq } from '../../../src/query/builder/functions.js' @@ -118,9 +119,8 @@ describe(`Subquery Types`, () => { // This should work WITHOUT any cast const _query = new Query() .from({ issue: issuesCollection }) - .join({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ) + .join(leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id))) .select(({ issue, activeUser }) => ({ issueId: issue.id, issueTitle: issue.title, @@ -150,9 +150,8 @@ describe(`Subquery Types`, () => { // This should work WITHOUT any cast const _query = new Query() .from({ issue: issuesCollection }) - .join({ activeUser: userNamesQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ) + .join(leftJoin({ activeUser: userNamesQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id))) .select(({ issue, activeUser }) => ({ issueId: issue.id, userName: activeUser.name, @@ -260,9 +259,8 @@ describe(`Subquery Types`, () => { // Join regular collection with subquery - NO CAST! const _query = new Query() .from({ issue: issuesCollection }) - .join({ activeUser: activeUsers }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ) + .join(leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id))) .select(({ issue, activeUser }) => ({ issueId: issue.id, userName: activeUser.name, @@ -286,9 +284,8 @@ describe(`Subquery Types`, () => { // Join subquery with regular collection - NO CAST! const _query = new Query() .from({ issue: filteredIssues }) - .join({ user: usersCollection }, ({ issue, user }) => - eq(issue.userId, user.id), - ) + .join(leftJoin({ user: usersCollection }, ({ issue, user }) => + eq(issue.userId, user.id))) .select(({ issue, user }) => ({ issueId: issue.id, userName: user.name, diff --git a/packages/db/tests/query/builder/union-all.test.ts b/packages/db/tests/query/builder/union-all.test.ts index ed08ab0a1a..9c96cbc93a 100644 --- a/packages/db/tests/query/builder/union-all.test.ts +++ b/packages/db/tests/query/builder/union-all.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { innerJoin } from '@tanstack/db' import { CollectionImpl } from '../../../src/collection/index.js' import { DbClient, collectionOptions } from '../../../src/client.js' import { @@ -117,12 +118,9 @@ describe(`QueryBuilder.unionAll`, () => { const query = builder .unionAll({ employees: employeeDescriptor }) - .join( - { departments: departmentDescriptor }, + .join(innerJoin({ departments: departmentDescriptor }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - `inner`, - ) + eq(employees.department_id, departments.id))) expect(collectSourceRefs(getQueryIR(query)).map((ref) => ref.type)).toEqual( [`descriptorRef`, `descriptorRef`], @@ -159,11 +157,8 @@ describe(`QueryBuilder.unionAll`, () => { const query = builder .unionAll(employeeRows, departmentRows) - .join( - { departments: departmentDescriptor }, - ({ id, departments }) => eq(id, departments.id), - `inner`, - ) + .join(innerJoin({ departments: departmentDescriptor }, + ({ id, departments }) => eq(id, departments.id))) expect(collectSourceRefs(getQueryIR(query)).map((ref) => ref.type)).toEqual( [`descriptorRef`, `descriptorRef`, `descriptorRef`], diff --git a/packages/db/tests/query/case-when.test-d.ts b/packages/db/tests/query/case-when.test-d.ts index 019e20e941..3444ade271 100644 --- a/packages/db/tests/query/case-when.test-d.ts +++ b/packages/db/tests/query/case-when.test-d.ts @@ -1,12 +1,11 @@ import { describe, expectTypeOf, test } from 'vitest' -import { createCollection } from '../../src/collection/index.js' -import { - caseWhen, +import { caseWhen, createLiveQueryCollection, eq, gt, - toArray, -} from '../../src/query/index.js' + leftJoin, + toArray } from '../../src/query/index.js' +import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -290,11 +289,8 @@ describe(`caseWhen types`, () => { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join( - { post: posts }, - ({ user, post }) => eq(user.id, post.userId), - `left`, - ) + .join(leftJoin({ post: posts }, + ({ user, post }) => eq(user.id, post.userId))) .select(({ user, post }) => ({ id: user.id, postStatus: caseWhen(post, `has-post`, `no-post`), diff --git a/packages/db/tests/query/case-when.test.ts b/packages/db/tests/query/case-when.test.ts index a5d8604b55..d4ff592833 100644 --- a/packages/db/tests/query/case-when.test.ts +++ b/packages/db/tests/query/case-when.test.ts @@ -1,13 +1,12 @@ import { describe, expect, test } from 'vitest' -import { createCollection } from '../../src/collection/index.js' -import { - caseWhen, - count, +import { caseWhen, count, createLiveQueryCollection, eq, gt, - toArray, -} from '../../src/query/index.js' + innerJoin, + leftJoin, + toArray } from '../../src/query/index.js' +import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' type User = { @@ -437,11 +436,8 @@ describe(`caseWhen`, () => { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join( - { post: posts }, - ({ user, post }) => eq(user.id, post.userId), - `left`, - ) + .join(leftJoin({ post: posts }, + ({ user, post }) => eq(user.id, post.userId))) .select(({ user, post }) => ({ id: user.id, title: caseWhen(post, post.title, `none`), @@ -468,12 +464,9 @@ describe(`caseWhen`, () => { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join( - { post: posts }, + .join(innerJoin({ post: posts }, ({ user, post }) => - eq(caseWhen(eq(user.active, true), user.id, -1), post.userId), - `inner`, - ) + eq(caseWhen(eq(user.active, true), user.id, -1), post.userId))) .select(({ user, post }) => ({ userId: user.id, title: post.title, diff --git a/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts b/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts index 8a82670b95..f71b8bb888 100644 --- a/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts +++ b/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts @@ -1,18 +1,17 @@ import fc from 'fast-check' import { Temporal } from 'temporal-polyfill' import { describe, expect, it } from 'vitest' +import { Query, and, createLiveQueryCollection, eq , + fullJoin as fullJoinOperator, + gt, + innerJoin, + joinOf, + leftJoin, + or } from '../../src/query/index.js' import { Func } from '../../src/query/ir.js' import { JoinConditionMustBeEqualityError } from '../../src/errors.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' -import { - Query, - and, - createLiveQueryCollection, - eq, - gt, - or, -} from '../../src/query/index.js' import { oraclePropertyOptions, oracleRuns, @@ -146,9 +145,8 @@ async function runColdJoin( const live = createLiveQueryCollection((q) => q .from({ parent: parentSource }) - .innerJoin({ child: children }, ({ parent, child }) => - eq(parent.id, child.parentId), - ) + .join(innerJoin({ child: children }, ({ parent, child }) => + eq(parent.id, child.parentId))) .select(({ parent, child }) => ({ id: child.id, parentId: parent.id, @@ -506,9 +504,8 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left }) - .innerJoin({ right }, ({ left: l, right: r }) => - eq(l.value, r.value), - ) + .join(innerJoin({ right }, ({ left: l, right: r }) => + eq(l.value, r.value))) .select(({ right: row }) => ({ id: row.id })), }) const filtered = createLiveQueryCollection({ @@ -570,9 +567,8 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left: binaryLeft }) - .innerJoin({ right: binaryRight }, ({ left, right }) => - eq(left.value, right.value), - ) + .join(innerJoin({ right: binaryRight }, ({ left, right }) => + eq(left.value, right.value))) .select(({ right }) => ({ id: right.id })), }) @@ -598,9 +594,8 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left: nullishLeft }) - .fullJoin({ right: nullishRight }, ({ left, right }) => - eq(left.value, right.value), - ) + .join(fullJoinOperator({ right: nullishRight }, ({ left, right }) => + eq(left.value, right.value))) .select(({ left, right }) => ({ leftId: left.id, rightId: right.id, @@ -659,9 +654,8 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left }) - .fullJoin({ right }, ({ left: l, right: r }) => - eq(l.value, r.value), - ) + .join(fullJoinOperator({ right }, ({ left: l, right: r }) => + eq(l.value, r.value))) .select(({ left: l, right: r }) => ({ leftId: l.id, rightId: r.id, @@ -798,9 +792,8 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left: active }) - .leftJoin({ right: lazy }, ({ left, right }) => - eq(left.binaryId, right.binaryId), - ) + .join(leftJoin({ right: lazy }, ({ left, right }) => + eq(left.binaryId, right.binaryId))) .select(({ left, right }) => ({ leftId: left.id, rightId: right.id, @@ -1123,8 +1116,7 @@ async function runCompoundHistory( const live = createLiveQueryCollection((q) => q .from({ left }) - .join( - { right }, + .join(joinOf(scenario.join, { right }, ({ left: l, right: r }) => { const a = scenario.reversed ? eq(r.a, l.a) : eq(l.a, r.a) const b = scenario.reversed ? eq(l.b, r.b) : eq(r.b, l.b) @@ -1133,9 +1125,7 @@ async function runCompoundHistory( if (scenario.width === 2) return and(first, second) if (scenario.nested) return and(first, and(second, c)) return and(first, second, c) - }, - scenario.join, - ) + })) .select(({ left: l, right: r }) => ({ leftId: l.id, rightId: r.id })), ) cleanupLive = () => live.cleanup() @@ -1408,13 +1398,13 @@ it.each([`or`, `inequality`, `empty`, `nested-invalid`] as const)( expect(() => new Query() .from({ left }) - .innerJoin({ right }, ({ left: l, right: r }) => { + .join(innerJoin({ right }, ({ left: l, right: r }) => { const equality = eq(l.id, r.id) if (shape === `or`) return or(equality, equality) if (shape === `inequality`) return gt(l.id, r.id) if (shape === `empty`) return new Func(`and`, []) return and(equality, and(equality, gt(l.id, r.id))) - }), + })), ).toThrow(JoinConditionMustBeEqualityError) return Promise.resolve() }, diff --git a/packages/db/tests/query/collection-ref-compatibility.test.ts b/packages/db/tests/query/collection-ref-compatibility.test.ts index 772ebc3af0..62fbf03a03 100644 --- a/packages/db/tests/query/collection-ref-compatibility.test.ts +++ b/packages/db/tests/query/collection-ref-compatibility.test.ts @@ -1,4 +1,6 @@ import { describe, expect, it } from 'vitest' +import { applyOptimizations } from '../../src/query/optimizer-joins.js' +import { processJoins } from '../../src/query/compiler/joins.js' import { IR, Query, @@ -104,7 +106,7 @@ describe(`CollectionRef public shape`, () => { query: { from: ref(`root`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: ref(`joined`), type: `inner`, on: new IR.Func(`eq`, [ diff --git a/packages/db/tests/query/compiler/basic.test.ts b/packages/db/tests/query/compiler/basic.test.ts index f3209ba94a..687c398efd 100644 --- a/packages/db/tests/query/compiler/basic.test.ts +++ b/packages/db/tests/query/compiler/basic.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from 'vitest' import { D2, MultiSet, output } from '@tanstack/db-ivm' +import { applyOptimizations } from '../../../src/query/optimizer-joins.js' +import { processJoins } from '../../../src/query/compiler/joins.js' import { compileQuery } from '../../../src/query/compiler/index.js' import { materializeCompilation } from '../../../src/query/live/materialized-pipeline.js' import { CollectionRef, Func, PropRef, Value } from '../../../src/query/ir.js' @@ -211,7 +213,7 @@ describe(`Query2 Compiler`, () => { const query: QueryIR = { from: new CollectionRef(usersCollection, userAlias), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `inner`, from: new CollectionRef(postsCollection, postAlias), on: new Func('eq', [ diff --git a/packages/db/tests/query/compiler/lazy-demand.test.ts b/packages/db/tests/query/compiler/lazy-demand.test.ts index ddd4d78fa9..21e54c26dd 100644 --- a/packages/db/tests/query/compiler/lazy-demand.test.ts +++ b/packages/db/tests/query/compiler/lazy-demand.test.ts @@ -1,5 +1,7 @@ import { D2, output } from '@tanstack/db-ivm' import { describe, expect, it } from 'vitest' +import { applyOptimizations } from '../../../src/query/optimizer-joins.js' +import { processJoins } from '../../../src/query/compiler/joins.js' import { createCollection } from '../../../src/collection/index.js' import { compileQuery } from '../../../src/query/compiler/index.js' import { CollectionRef, Func, PropRef } from '../../../src/query/ir.js' @@ -29,7 +31,7 @@ function createDemandHarness( { from: new CollectionRef(left, `left`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: joinType, from: new CollectionRef(right, `right`), on: diff --git a/packages/db/tests/query/compiler/subqueries.test.ts b/packages/db/tests/query/compiler/subqueries.test.ts index 3bf3a6f99f..3e220ad8be 100644 --- a/packages/db/tests/query/compiler/subqueries.test.ts +++ b/packages/db/tests/query/compiler/subqueries.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { D2, MultiSet, output } from '@tanstack/db-ivm' +import { leftJoin } from '@tanstack/db' import { Query, getQueryIR } from '../../../src/query/builder/index.js' import { compileQuery } from '../../../src/query/compiler/index.js' import { CollectionImpl } from '../../../src/collection/index.js' @@ -223,9 +224,8 @@ describe(`Query2 Subqueries`, () => { // Use the subquery in a join const query = new Query() .from({ issue: issuesCollection }) - .join({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ) + .join(leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id))) .select(({ issue, activeUser }) => ({ issueId: issue.id, issueTitle: issue.title, @@ -258,9 +258,8 @@ describe(`Query2 Subqueries`, () => { // Use the subquery in a join const query = new Query() .from({ issue: issuesCollection }) - .join({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ) + .join(leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id))) .select(({ issue, activeUser }) => ({ issueId: issue.id, issueTitle: issue.title, @@ -362,9 +361,8 @@ describe(`Query2 Subqueries`, () => { const outerQuery = new Query() .from({ issue: issuesCollection }) - .join({ userInfo: middleQuery }, ({ issue, userInfo }) => - eq(issue.userId, userInfo.id), - ) + .join(leftJoin({ userInfo: middleQuery }, ({ issue, userInfo }) => + eq(issue.userId, userInfo.id))) .select(({ issue, userInfo }) => ({ issueId: issue.id, issueTitle: issue.title, diff --git a/packages/db/tests/query/compiler/subquery-caching.test.ts b/packages/db/tests/query/compiler/subquery-caching.test.ts index 8b2188df20..089642e3f2 100644 --- a/packages/db/tests/query/compiler/subquery-caching.test.ts +++ b/packages/db/tests/query/compiler/subquery-caching.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest' import { D2 } from '@tanstack/db-ivm' +import { applyOptimizations } from '../../../src/query/optimizer-joins.js' +import { processJoins } from '../../../src/query/compiler/joins.js' import { compileQuery } from '../../../src/query/compiler/index.js' import { queriesMatchForCaching } from '../../../src/query/compiler/query-equivalence.js' import { @@ -103,7 +105,7 @@ describe(`Subquery Caching`, () => { const mainQuery: QueryIR = { from: new QueryRef(subquery, `main_users`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `inner`, from: new QueryRef(subquery, `joined_users`), // Same subquery object reference on: new Func('eq', [ @@ -379,7 +381,7 @@ describe(`Subquery Caching`, () => { const middleSubquery: QueryIR = { from: new QueryRef(innerSubquery, `inner1`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `left`, from: new QueryRef(innerSubquery, `inner2`), // Same innerSubquery on: new Func('eq', [ @@ -393,7 +395,7 @@ describe(`Subquery Caching`, () => { const outerQuery: QueryIR = { from: new QueryRef(middleSubquery, `middle`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `inner`, from: new QueryRef(innerSubquery, `direct`), // innerSubquery again at top level on: new Func('eq', [ diff --git a/packages/db/tests/query/composables.test.ts b/packages/db/tests/query/composables.test.ts index e5744dde63..cadec16d7b 100644 --- a/packages/db/tests/query/composables.test.ts +++ b/packages/db/tests/query/composables.test.ts @@ -1,15 +1,14 @@ import { beforeEach, describe, expect, test } from 'vitest' -import { - Query, +import { Query, and, createLiveQueryCollection, eq, gt, + innerJoin, lower, lt, lte, - upper, -} from '../../src/query/index.js' + upper } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { Ref } from '../../src/query/index.js' @@ -269,11 +268,8 @@ describe(`Composables`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - { post: postsCollection }, - ({ user, post }) => eq(user.id, post.authorId), - `inner`, - ) + .join(innerJoin({ post: postsCollection }, + ({ user, post }) => eq(user.id, post.authorId))) .where(userIsActive) .where(postIsPublished) .select(userPostJoinInfo), @@ -423,11 +419,8 @@ describe(`Composables`, () => { query: (q) => q .from({ u: usersCollection }) - .join( - { p: postsCollection }, - ({ u, p }) => eq(u.id, p.authorId), - `inner`, - ) + .join(innerJoin({ p: postsCollection }, + ({ u, p }) => eq(u.id, p.authorId))) .where(joinedUserPostPredicate) .select(joinedSelect), startSync: true, diff --git a/packages/db/tests/query/distinct.test.ts b/packages/db/tests/query/distinct.test.ts index 6a09460c34..68f3748707 100644 --- a/packages/db/tests/query/distinct.test.ts +++ b/packages/db/tests/query/distinct.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, test } from 'vitest' -import { concat, createLiveQueryCollection } from '../../src/query/index.js' +import { concat, createLiveQueryCollection, leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import { DistinctRequiresSelectError } from '../../src/errors' @@ -696,11 +696,9 @@ function createDistinctTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ users: usersCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ users, departments }) => - eq(users.department, departments.id), - ) + eq(users.department, departments.id))) .where(({ users }) => eq(users.active, true)) .select(({ departments }) => ({ department: departments.id, diff --git a/packages/db/tests/query/findone-joins.test-d.ts b/packages/db/tests/query/findone-joins.test-d.ts index 9f76853dd6..e45a7fff6f 100644 --- a/packages/db/tests/query/findone-joins.test-d.ts +++ b/packages/db/tests/query/findone-joins.test-d.ts @@ -1,5 +1,5 @@ import { describe, expectTypeOf, test } from 'vitest' -import { createLiveQueryCollection, eq } from '../../src/query/index.js' +import { createLiveQueryCollection, eq, fullJoin, innerJoin, leftJoin, rightJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -45,10 +45,8 @@ describe(`findOne() with joins`, () => { .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) .orderBy(({ todo }) => todo.order, `asc`) - .leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ) + .join(leftJoin({ todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) .findOne(), }) @@ -68,10 +66,8 @@ describe(`findOne() with joins`, () => { q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) - .innerJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ) + .join(innerJoin({ todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) .findOne(), }) @@ -92,10 +88,8 @@ describe(`findOne() with joins`, () => { .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) .findOne() - .leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ), + .join(leftJoin({ todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))), }) expectTypeOf(query.toArray).toMatchTypeOf< @@ -114,10 +108,8 @@ describe(`findOne() with joins`, () => { q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) - .rightJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ) + .join(rightJoin({ todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) .findOne(), }) @@ -137,10 +129,8 @@ describe(`findOne() with joins`, () => { q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) - .fullJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ) + .join(fullJoin({ todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) .findOne(), }) @@ -173,13 +163,10 @@ describe(`findOne() with joins`, () => { query: (q) => q .from({ todo: todoCollection }) - .leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ) - .innerJoin({ tag: todoTagsCollection }, ({ todo, tag }) => - eq(todo.id, tag.todoId), - ) + .join(leftJoin({ todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) + .join(innerJoin({ tag: todoTagsCollection }, ({ todo, tag }) => + eq(todo.id, tag.todoId))) .findOne(), }) @@ -199,10 +186,8 @@ describe(`findOne() with joins`, () => { query: (q) => q .from({ todo: todoCollection }) - .leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ) + .join(leftJoin({ todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) .select(({ todo, todoOptions }) => ({ todoText: todo.text, optionText: todoOptions.optionText, @@ -225,10 +210,8 @@ describe(`findOne() with joins`, () => { query: (q) => q .from({ todo: todoCollection }) - .leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ) + .join(leftJoin({ todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) .findOne() .select(({ todo, todoOptions }) => ({ todoText: todo.text, @@ -253,10 +236,8 @@ describe(`findOne() with joins`, () => { .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) .orderBy(({ todo }) => todo.order, `asc`) - .leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ) + .join(leftJoin({ todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) .limit(1), }) diff --git a/packages/db/tests/query/functional-variants.test-d.ts b/packages/db/tests/query/functional-variants.test-d.ts index a388579f48..5b168970d4 100644 --- a/packages/db/tests/query/functional-variants.test-d.ts +++ b/packages/db/tests/query/functional-variants.test-d.ts @@ -1,14 +1,13 @@ import { describe, expectTypeOf, test } from 'vitest' -import { - Query, +import { Query, caseWhen, count, createLiveQueryCollection, eq, gt, + leftJoin, materialize, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -271,9 +270,8 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .fn.select((row) => ({ employeeInfo: `${row.user.name} works in ${row.dept?.name || `Unknown`}`, isHighEarner: row.user.salary > 70000, @@ -306,9 +304,8 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .fn.where( (row) => row.user.active && (row.dept?.name === `Engineering` || false), @@ -332,9 +329,8 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .fn.where((row) => row.user.active) .fn.where((row) => row.user.salary > 60000) .fn.select((row) => ({ @@ -531,9 +527,8 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .groupBy(({ dept }) => dept.name) .fn.having((row) => row.dept?.name !== `HR`) .select(({ dept, user }) => ({ diff --git a/packages/db/tests/query/functional-variants.test.ts b/packages/db/tests/query/functional-variants.test.ts index f803e1f04e..8f0655acb7 100644 --- a/packages/db/tests/query/functional-variants.test.ts +++ b/packages/db/tests/query/functional-variants.test.ts @@ -1,13 +1,12 @@ import { beforeEach, describe, expect, test } from 'vitest' -import { - caseWhen, +import { caseWhen, count, createLiveQueryCollection, eq, gt, + leftJoin, materialize, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' @@ -192,9 +191,8 @@ describe(`Functional Variants Query`, () => { query: (q) => q .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .fn.select((row) => ({ employeeInfo: `${row.user.name} works in ${row.dept?.name || `Unknown`}`, isHighEarner: row.user.salary > 70000, @@ -850,9 +848,8 @@ describe(`Functional Variants Query`, () => { query: (q) => q .from({ user: usersCollection }) - .join({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .fn.where((row) => row.user.active) .fn.where((row) => row.user.salary > 60000) .fn.select((row) => ({ diff --git a/packages/db/tests/query/generic-collection.test-d.ts b/packages/db/tests/query/generic-collection.test-d.ts index 553b5f975b..b50a04b6f2 100644 --- a/packages/db/tests/query/generic-collection.test-d.ts +++ b/packages/db/tests/query/generic-collection.test-d.ts @@ -1,5 +1,5 @@ import { describe, test } from 'vitest' -import { createLiveQueryCollection, eq } from '../../src/query/index.js' +import { createLiveQueryCollection, eq, leftJoin } from '../../src/query/index.js' import type { Collection } from '../../src/collection/index.js' // Regression tests for https://github.com/TanStack/db/issues/1677 @@ -42,9 +42,8 @@ describe(`queries over generic collection row types`, () => { const sub = q.from({ a }) return q .from({ b }) - .leftJoin({ sub }, ({ b: bRef, sub: subRef }) => - eq(bRef.id, subRef.id), - ) + .join(leftJoin({ sub }, ({ b: bRef, sub: subRef }) => + eq(bRef.id, subRef.id))) }) } diff --git a/packages/db/tests/query/group-by.test.ts b/packages/db/tests/query/group-by.test.ts index ebb82bc23c..13d29c49bb 100644 --- a/packages/db/tests/query/group-by.test.ts +++ b/packages/db/tests/query/group-by.test.ts @@ -8,7 +8,7 @@ */ import { beforeEach, describe, expect, test } from 'vitest' import { Temporal } from 'temporal-polyfill' -import { createLiveQueryCollection, toArray } from '../../src/query/index.js' +import { createLiveQueryCollection, leftJoin, toArray } from '../../src/query/index.js' import { NonAggregateExpressionNotInGroupByError } from '../../src/errors.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' @@ -2415,9 +2415,8 @@ function createGroupByTests(autoIndex: `off` | `eager`): void { return q .from({ customer: customersCollection }) - .leftJoin({ oc: orderCountSubquery }, ({ customer, oc }) => - eq(customer.id, oc.customer_id), - ) + .join(leftJoin({ oc: orderCountSubquery }, ({ customer, oc }) => + eq(customer.id, oc.customer_id))) .select(({ customer, oc }) => ({ name: customer.name, orderCount: oc.orderCount, diff --git a/packages/db/tests/query/identity-output-shape-oracle.test.ts b/packages/db/tests/query/identity-output-shape-oracle.test.ts index 0158b72e5e..428117697a 100644 --- a/packages/db/tests/query/identity-output-shape-oracle.test.ts +++ b/packages/db/tests/query/identity-output-shape-oracle.test.ts @@ -3,18 +3,19 @@ import { D2, MultiSet, output, serializeValue } from '@tanstack/db-ivm' import { Temporal } from 'temporal-polyfill' import fc from 'fast-check' import { describe, expect, it } from 'vitest' +import { Query, + and, + createLiveQueryCollection, + eq, + innerJoin } from '../../src/query/index.js' +import { processJoins } from '../../src/query/compiler/joins.js' +import { applyOptimizations } from '../../src/query/optimizer-joins.js' import { getQueryIR } from '../../src/query/builder/query-ir.js' import { createCollection } from '../../src/collection/index.js' import { count as countRows } from '../../src/query/builder/functions.js' import { compileExpression } from '../../src/query/compiler/evaluators.js' import { compileQuery } from '../../src/query/compiler/index.js' import { getEqualityValueIdentity } from '../../src/query/equality-value-identity.js' -import { - Query, - and, - createLiveQueryCollection, - eq, -} from '../../src/query/index.js' import { queriesMatchForCaching } from '../../src/query/compiler/query-equivalence.js' import { getLoadSubsetDemandKey, @@ -155,6 +156,7 @@ async function runShapes( from: user, join: [ { + compile: processJoins, optimize: applyOptimizations, type: 'inner', from: post, on: new Func('eq', [ @@ -667,14 +669,14 @@ describe(`compound join identity and output`, () => { ) => { const joined = new Query() .from({ left }) - .innerJoin({ right }, ({ left: l, right: r }) => { + .join(innerJoin({ right }, ({ left: l, right: r }) => { const a = eq(l.a, r.a) const b = eq(l.b, variant === `different` ? r.c : r.b) if (variant === `reordered`) return and(eq(r.b, l.b), eq(r.a, l.a)) if (variant === `nested`) return and(b, and(a, b)) return and(a, b) - }) + })) .select(({ left: l, right: r }) => ({ leftId: l.id, rightId: r.id, @@ -698,9 +700,8 @@ describe(`compound join identity and output`, () => { } const query = new Query() .from({ anchor: left }) - .innerJoin({ result: joined }, ({ anchor, result }) => - eq(anchor.id, result.leftId), - ) + .join(innerJoin({ result: joined }, ({ anchor, result }) => + eq(anchor.id, result.leftId))) .select(({ result }) => ({ leftId: result.leftId, rightId: result.rightId, diff --git a/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts b/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts index 5f4e9d49fa..47dfe65fcb 100644 --- a/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts +++ b/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts @@ -1,16 +1,15 @@ import { describe, expect, test } from 'vitest' -import { - Query, - add, - caseWhen, +import { Query, add, caseWhen , count, createLiveQueryCollection, eq, gt, + innerJoin, isUndefined, + joinOf, + leftJoin, multiply, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { getQueryIR } from '../../src/query/builder/query-ir.js' import { IncludesSubquery, PropRef } from '../../src/query/ir.js' import { getQueryIdentity } from '../../src/query/ir-stable-identity.js' @@ -236,17 +235,15 @@ function buildQuery( return toArray( new Query() .unionAll(left, right) - .innerJoin({ anchor: locks.collection }, ({ lockId, anchor }) => - eq(lockId, anchor.id), - ) + .join(innerJoin({ anchor: locks.collection }, ({ lockId, anchor }) => + eq(lockId, anchor.id))) .where(({ anchor }) => eq(anchor.id, parent.id)) .select(({ id, lockId }) => ({ id, lockId })), ) } return toArray( - base().innerJoin({ other: locks.collection }, (context: Context) => - eq(voteAt(context, alias).lockId, (context.other as Lock).id), - ), + base().join(innerJoin({ other: locks.collection }, (context: Context) => + eq(voteAt(context, alias).lockId, (context.other as Lock).id))), ) })() return { id: parent.id, children } @@ -462,9 +459,8 @@ describe(`captured alias scope oracle`, () => { return createLiveQueryCollection({ query: new Query() .from({ manager: managers.collection }) - .innerJoin({ helper: shared }, ({ manager, helper }) => - eq(manager.id, helper.id), - ) + .join(innerJoin({ helper: shared }, ({ manager, helper }) => + eq(manager.id, helper.id))) .select(({ manager }) => reportsFor(manager as unknown as Employee), ), @@ -623,9 +619,8 @@ describe(`captured alias scope oracle`, () => { const live = createLiveQueryCollection({ query: new Query() .unionAll(first, second) - .innerJoin({ anchor: source.collection }, ({ id, anchor }) => - eq(id, anchor.id), - ) + .join(innerJoin({ anchor: source.collection }, ({ id, anchor }) => + eq(id, anchor.id))) .select(({ anchor }) => ({ id: anchor.id, children: toArray( @@ -665,9 +660,8 @@ describe(`captured alias scope oracle`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ wrapped: inner }) - .innerJoin({ anchor: source.collection }, ({ wrapped, anchor }) => - eq(wrapped.id, anchor.id), - ) + .join(innerJoin({ anchor: source.collection }, ({ wrapped, anchor }) => + eq(wrapped.id, anchor.id))) .select(({ anchor }) => ({ id: anchor.id, children: toArray( @@ -803,10 +797,8 @@ describe(`captured alias scope oracle`, () => { children: toArray( new Query() .from({ lock: votes.collection }) - .innerJoin( - { other: locks.collection }, - ({ lock: child, other }) => eq(child.lockId, other.id), - ) + .join(innerJoin({ other: locks.collection }, + ({ lock: child, other }) => eq(child.lockId, other.id))) .where(({ lock: child, other }) => eq(child.lockId, other.id)) .where(({ lock: child }) => eq(child.lockId, parent.id)) .select(({ lock: child }) => ({ @@ -1290,9 +1282,8 @@ describe(`outer filters stay with their source under alias shadowing`, () => { form === `join` ? createLiveQueryCollection({ query: root - .leftJoin({ w: childQuery }, ({ u, w }) => - eq(u.linkId, w.id), - ) + .join(leftJoin({ w: childQuery }, ({ u, w }) => + eq(u.linkId, w.id))) .select(({ u, w }) => ({ id: u.id, joinedId: w.id })), }) : createLiveQueryCollection({ @@ -1437,9 +1428,8 @@ describe(`captured projections keep their binding during predicate pushdown`, () matches: toArray( new Query() .from({ q: inner }) - .innerJoin({ a: anchors.collection }, ({ q, a }) => - eq(q.id, a.id), - ) + .join(innerJoin({ a: anchors.collection }, ({ q, a }) => + eq(q.id, a.id))) .where(({ q }) => eq(q.parentId, parent.id)) .where(({ q }) => eq(q.rank, `A`)) .select(({ q }) => ({ id: q.id })), @@ -1705,9 +1695,8 @@ describe(`group keys through nested source filtering`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ x: inner }) - .innerJoin({ t: tags.collection }, ({ x, t }) => - eq(x.id, t.sourceId), - ) + .join(innerJoin({ t: tags.collection }, ({ x, t }) => + eq(x.id, t.sourceId))) .where(({ x }) => eq(x.category, `A`)) .groupBy(({ x }) => x.category) .select(({ x }) => ({ @@ -1987,7 +1976,7 @@ describe(`lazy join demand across nested alias scopes`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ a: anchors.collection }) - .leftJoin({ w: inner }, ({ a, w }) => eq(a.userId, w.id)) + .join(leftJoin({ w: inner }, ({ a, w }) => eq(a.userId, w.id))) .select(({ a, w }) => ({ anchorId: a.id, userId: w.id, @@ -2111,9 +2100,8 @@ describe(`lexical bindings through recursive joins and selections`, () => { return createLiveQueryCollection({ query: new Query() .from({ manager: managers }) - .leftJoin({ user: people.collection }, ({ manager, user }) => - eq(manager.id, user.managerId), - ) + .join(leftJoin({ user: people.collection }, ({ manager, user }) => + eq(manager.id, user.managerId))) .where(({ user }) => eq(user.managerId, 1)) .select(({ manager, user }) => ({ managerId: manager.id, @@ -2184,9 +2172,8 @@ describe(`lexical bindings through recursive joins and selections`, () => { return createLiveQueryCollection({ query: new Query() .from({ manager: inner }) - .leftJoin({ user: users.collection }, ({ manager, user }) => - eq(manager.id, user.managerId), - ) + .join(leftJoin({ user: users.collection }, ({ manager, user }) => + eq(manager.id, user.managerId))) .select(({ manager, user }) => ({ managerId: manager.id, userId: user.id, @@ -2302,12 +2289,11 @@ describe(`lexical bindings through recursive joins and selections`, () => { children: toArray( new Query() .from({ [childAlias]: children.collection }) - .innerJoin({ anchor: anchorQuery }, (context: Context) => + .join(innerJoin({ anchor: anchorQuery }, (context: Context) => eq( (context[childAlias] as { anchorId: number }).anchorId, (context.anchor as { id: number }).id, - ), - ) + ))) .where((context: Context) => eq( (context[childAlias] as { parentId: number }).parentId, @@ -2377,14 +2363,12 @@ describe(`lexical bindings through recursive joins and selections`, () => { children: toArray( new Query() .from({ comment: comments.collection }) - .leftJoin( - { [joinAlias]: anchors.collection }, + .join(leftJoin({ [joinAlias]: anchors.collection }, (context: Context) => eq( (context.comment as { anchorId: number }).anchorId, (context[joinAlias] as { id: number }).id, - ), - ) + ))) .where((context: Context) => eq( (context.comment as { parentId: number }).parentId, @@ -2855,10 +2839,8 @@ describe(`nested source placement preserves captured outer rows`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ manager: managers.collection }) - .innerJoin( - { user: assignments.collection }, - ({ manager, user }) => eq(manager.id, user.managerId), - ) + .join(innerJoin({ user: assignments.collection }, + ({ manager, user }) => eq(manager.id, user.managerId))) .select(({ manager, user }) => { const project = (context: Context) => { const detail = context[alias] as Detail @@ -2883,14 +2865,12 @@ describe(`nested source placement preserves captured outer rows`, () => { .select(project) : new Query() .from({ anchor: anchors.collection }) - .innerJoin( - { [alias]: details.collection }, + .join(innerJoin({ [alias]: details.collection }, (context: Context) => eq( (context[alias] as Detail).managerId, (context.anchor as { id: number }).id, - ), - ) + ))) .where((context: Context) => eq( (context.anchor as { id: number }).id, @@ -3184,7 +3164,7 @@ describe(`outer joins preserve absent source bindings under shadowing`, () => { ) const anchorSource = sourceForm === `direct` ? anchors.collection : scopedAnchors - const joined = main.join({ anchor: anchorSource }, on, kind) + const joined = main.join(joinOf(kind, { anchor: anchorSource }, on)) expect(getQueryIR(joined).join?.[0]?.from.type).toBe( sourceForm === `direct` ? `collectionRef` : `queryRef`, ) @@ -3446,15 +3426,13 @@ describe(`chained joins keep an absent local source absent`, () => { rows: toArray( new Query() .from({ comment: comments.collection }) - .leftJoin( - { [alias]: localIssues.collection }, + .join(leftJoin({ [alias]: localIssues.collection }, (context: Context) => eq( (context.comment as Comment).localIssueId, (context[alias] as Issue).id, - ), - ) - .leftJoin({ tag: tagSource }, (context: Context) => + ))) + .join(leftJoin({ tag: tagSource }, (context: Context) => routeCause === `queryRef` ? eq( (context.comment as Comment).tagId, @@ -3466,8 +3444,7 @@ describe(`chained joins keep an absent local source absent`, () => { parent.id, ), add((context.tag as Tag).id, parent.id), - ), - ) + ))) .where((context: Context) => eq((context.comment as Comment).issueId, parent.id), ) @@ -3648,9 +3625,8 @@ describe(`joined QueryRef lazy targets retain lexical source identity`, () => { }) const joined = new Query() .from({ [alias]: posts.collection }) - .innerJoin({ tag: tags.collection }, (context: Context) => - eq((context[alias] as Post).tagId, (context.tag as Tag).id), - ) + .join(innerJoin({ tag: tags.collection }, (context: Context) => + eq((context[alias] as Post).tagId, (context.tag as Tag).id))) .select((context: Context) => ({ tagId: (context.tag as Tag).id, title: (context[alias] as Post).title, @@ -3659,7 +3635,7 @@ describe(`joined QueryRef lazy targets retain lexical source identity`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ u: users.collection }) - .leftJoin({ p: joined }, ({ u, p }) => eq(u.tagId, p.tagId)) + .join(leftJoin({ p: joined }, ({ u, p }) => eq(u.tagId, p.tagId))) .select(({ u, p }) => ({ id: u.id, name: u.name, @@ -3913,24 +3889,18 @@ describe(`mixed outer-join chains retain absent source roles`, () => { rows: toArray( new Query() .from({ [mainAlias]: mains.collection }) - .join( - { [anchorAlias]: anchors.collection }, + .join(joinOf(first, { [anchorAlias]: anchors.collection }, (context: Context) => eq( (context[mainAlias] as Main).anchorId, (context[anchorAlias] as Anchor).id, - ), - first, - ) - .join( - { tag: tags.collection }, + ))) + .join(joinOf(second, { tag: tags.collection }, (context: Context) => eq( (context[anchorAlias] as Anchor).id, (context.tag as Tag).anchorId, - ), - second, - ) + ))) .where((context: Context) => correlation === `main` ? eq((context[mainAlias] as Main).parentId, parent.id) @@ -4124,8 +4094,7 @@ describe(`captured ancestor join operands retain their bindings`, () => { rows: toArray( new Query() .from({ child: children.collection }) - .innerJoin( - { [alias]: targets.collection }, + .join(innerJoin({ [alias]: targets.collection }, (context: Context) => { const child = context.child as Child const target = context[alias] as Target @@ -4142,8 +4111,7 @@ describe(`captured ancestor join operands retain their bindings`, () => { return order === `childFirst` ? eq(mainKey, joinedKey) : eq(joinedKey, mainKey) - }, - ) + })) .where((context: Context) => eq((context.child as Child).parentId, ancestor.id), ) @@ -4302,9 +4270,8 @@ describe(`join operands reject captured sources outside lexical scope`, () => { const child = (operand: number) => new Query() .from({ local: local.collection }) - .innerJoin({ joined: joined.collection }, ({ joined: target }) => - eq(operand, target.id), - ) + .join(innerJoin({ joined: joined.collection }, ({ joined: target }) => + eq(operand, target.id))) .select(({ local: own, joined: target }) => ({ localId: own.id, joinedId: target.id, @@ -4336,10 +4303,8 @@ describe(`join operands reject captured sources outside lexical scope`, () => { const localControl = createLiveQueryCollection({ query: new Query() .from({ local: local.collection }) - .innerJoin( - { joined: joined.collection }, - ({ local: own, joined: target }) => eq(own.id, target.id), - ) + .join(innerJoin({ joined: joined.collection }, + ({ local: own, joined: target }) => eq(own.id, target.id))) .select(({ local: own, joined: target }) => ({ localId: own.id, joinedId: target.id, @@ -4933,10 +4898,8 @@ describe(`include joins reject sources hidden inside parent operands`, () => { rows: toArray( new Query() .from({ local: local.collection }) - .innerJoin( - { joined: joined.collection }, - ({ joined: target }) => eq(hiddenRef, target.id), - ) + .join(innerJoin({ joined: joined.collection }, + ({ joined: target }) => eq(hiddenRef, target.id))) .where(({ local: row }) => eq(row.id, parentId)) .select(({ local: row, joined: target }) => ({ localId: row.id, @@ -5023,16 +4986,14 @@ describe(`joined subqueries reject captured sibling sources`, () => { }) const joinedQuery = new Query() .from({ [alias]: inner.collection }) - .innerJoin({ probe: probe.collection }, ({ probe: row }) => - eq(siblingRef, row.id), - ) + .join(innerJoin({ probe: probe.collection }, ({ probe: row }) => + eq(siblingRef, row.id))) .select((context: Context) => ({ id: (context[alias] as Row).id, })) const child = base - .innerJoin({ nested: joinedQuery }, ({ local: row, nested }) => - eq(row.id, nested.id), - ) + .join(innerJoin({ nested: joinedQuery }, ({ local: row, nested }) => + eq(row.id, nested.id))) .select(({ local: row, nested }) => ({ localId: row.id, joinedId: nested.id, @@ -5126,9 +5087,8 @@ describe(`joined subqueries reject captured sibling sources`, () => { .where((context: Context) => eq((context[alias] as Row).id, ancestor.id), ) - .innerJoin({ probe: probe.collection }, ({ probe: row }) => - eq(ancestor.id, row.id), - ) + .join(innerJoin({ probe: probe.collection }, ({ probe: row }) => + eq(ancestor.id, row.id))) .select((context: Context) => ({ id: (context[alias] as Row).id, ancestorId: ancestor.id, @@ -5138,10 +5098,8 @@ describe(`joined subqueries reject captured sibling sources`, () => { rows: toArray( new Query() .from({ local: main.collection }) - .innerJoin( - { nested: joinedQuery }, - ({ local: row, nested }) => eq(row.id, nested.id), - ) + .join(innerJoin({ nested: joinedQuery }, + ({ local: row, nested }) => eq(row.id, nested.id))) .where(({ local: row }) => eq(row.id, ancestor.id)) .select(({ local: row, nested }) => ({ localId: row.id, @@ -5207,9 +5165,8 @@ test(`mixed bound and unbound same-alias join references name separate sources`, rows: toArray( new Query() .from({ child: child.collection }) - .innerJoin({ issue: local.collection }, () => - eq(ancestor.id, new PropRef([`issue`, `id`])), - ) + .join(innerJoin({ issue: local.collection }, () => + eq(ancestor.id, new PropRef([`issue`, `id`])))) .where(({ child: row }) => eq(row.parentId, ancestor.id)) .select(({ child: row, issue }) => ({ childId: row.id, diff --git a/packages/db/tests/query/includes-collection-oracle.property.test.ts b/packages/db/tests/query/includes-collection-oracle.property.test.ts index b162ba7e5e..f107eff592 100644 --- a/packages/db/tests/query/includes-collection-oracle.property.test.ts +++ b/packages/db/tests/query/includes-collection-oracle.property.test.ts @@ -1,21 +1,20 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect } from 'vitest' -import { createCollection } from '../../src/collection/index.js' -import { BTreeIndex } from '../../src/indexes/btree-index.js' -import { - add, - caseWhen, +import { add, caseWhen, concat, count, createLiveQueryCollection, eq, gt, + innerJoin, + leftJoin, lt, materialize, multiply, sum, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' +import { createCollection } from '../../src/collection/index.js' +import { BTreeIndex } from '../../src/indexes/btree-index.js' import { runTrace } from '../trace-runner.js' import { withHistoryCleanup } from '../optimistic-history-oracle.js' import { @@ -1663,9 +1662,8 @@ describe(`Collection-valued includes oracle`, () => { query: (q) => { const joined = q .from({ comment: comments.collection }) - .join({ user: users.collection }, ({ comment, user }) => - eq(comment.userId, user.id), - ) + .join(leftJoin({ user: users.collection }, ({ comment, user }) => + eq(comment.userId, user.id))) return mode === `value` ? joined.select(({ comment }) => ({ publicId: comment.userId, @@ -3109,9 +3107,8 @@ describe(`Collection-valued includes oracle`, () => { const childRows = () => q .from({ child: children.collection }) - .innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(tag.id, add(child.tagId, parent.offset)), - ) + .join(innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(tag.id, add(child.tagId, parent.offset)))) .where(({ child }) => eq(child.parentGroup, parent.group)) .select(({ child, tag }) => ({ id: child.id, diff --git a/packages/db/tests/query/includes-context-transport-oracle.test.ts b/packages/db/tests/query/includes-context-transport-oracle.test.ts index 4768204add..4d5134929d 100644 --- a/packages/db/tests/query/includes-context-transport-oracle.test.ts +++ b/packages/db/tests/query/includes-context-transport-oracle.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from 'vitest' -import { - add, +import { add, and, caseWhen, coalesce, @@ -8,13 +7,13 @@ import { createLiveQueryCollection, eq, gt, + innerJoin, lt, lte, materialize, multiply, sum, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { attachRouteMetadata, stripInternalRouteMetadata, @@ -158,9 +157,8 @@ test('keeps dotted ancestor aliases distinct in nested includes', async () => { const live = createLiveQueryCollection((q) => q .from({ 'p.q': dotted.collection }) - .innerJoin({ p: nested.collection }, ({ 'p.q': left, p }) => - eq(left.id, p.id), - ) + .join(innerJoin({ p: nested.collection }, ({ 'p.q': left, p }) => + eq(left.id, p.id))) .select(({ 'p.q': left, p }) => ({ id: left.id, children: toArray( @@ -1228,10 +1226,8 @@ async function runRecursiveSourceCell({ case `joined-query-ref`: { const routed = q .from({ anchor: anchors.collection }) - .innerJoin( - { result: buildCandidates(candidates.collection) }, - ({ anchor, result }) => eq(anchor.id, result.id), - ) + .join(innerJoin({ result: buildCandidates(candidates.collection) }, + ({ anchor, result }) => eq(anchor.id, result.id))) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ result }) => ({ id: result.id, @@ -1242,9 +1238,8 @@ async function runRecursiveSourceCell({ case `union-branch`: { const routed = q .unionAll(buildLeftCandidates(), buildRightCandidates()) - .innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => - eq(id, anchor.id), - ) + .join(innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => + eq(id, anchor.id))) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ id, value }) => ({ id, value })) return { id: parent.id, ...includeInEveryForm(routed) } @@ -1388,9 +1383,8 @@ async function runUnionIdentityCell({ })) return q .unionAll(leftRows, rightRows) - .innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => - eq(id, anchor.id), - ) + .join(innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => + eq(id, anchor.id))) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ id, value }) => ({ id, value })) } @@ -1400,11 +1394,9 @@ async function runUnionIdentityCell({ leftCandidate: left.collection, rightCandidate: right.collection, }) - .innerJoin( - { anchor: anchors.collection }, + .join(innerJoin({ anchor: anchors.collection }, ({ leftCandidate, rightCandidate, anchor }) => - eq(coalesce(leftCandidate.id, rightCandidate.id), anchor.id), - ) + eq(coalesce(leftCandidate.id, rightCandidate.id), anchor.id))) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ leftCandidate, rightCandidate }) => ({ id: coalesce(leftCandidate.id, rightCandidate.id), @@ -1530,9 +1522,8 @@ async function runDerivedResultCell({ ) const rows = q .from({ anchor: anchors.collection }) - .innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value), - ) + .join(innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value))) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1545,9 +1536,8 @@ async function runDerivedResultCell({ if (boundary === `from-query-ref`) { const rows = q .from({ value: values }) - .innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => - eq(value, anchor.value), - ) + .join(innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => + eq(value, anchor.value))) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1555,9 +1545,8 @@ async function runDerivedResultCell({ const rows = q .from({ anchor: anchors.collection }) - .innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value), - ) + .join(innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value))) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1569,9 +1558,8 @@ async function runDerivedResultCell({ if (boundary === `from-query-ref`) { const rows = q .from({ value: values }) - .innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => - eq(value, anchor.value), - ) + .join(innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => + eq(value, anchor.value))) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1579,9 +1567,8 @@ async function runDerivedResultCell({ const rows = q .from({ anchor: anchors.collection }) - .innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value), - ) + .join(innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value))) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1701,11 +1688,9 @@ async function runQueryRefMetadataCell( })) return q .from({ anchor: anchors.collection }) - .innerJoin( - { candidateResult: projectedCandidates }, + .join(innerJoin({ candidateResult: projectedCandidates }, ({ anchor, candidateResult }) => - eq(anchor.candidateId, candidateResult.id), - ) + eq(anchor.candidateId, candidateResult.id))) .select(({ candidateResult }) => candidateResult) }) @@ -1736,11 +1721,9 @@ async function runQueryRefMetadataCell( })) const rows = q .from({ anchor: anchors.collection }) - .innerJoin( - { candidateResult: projectedCandidates }, + .join(innerJoin({ candidateResult: projectedCandidates }, ({ anchor, candidateResult }) => - eq(anchor.candidateId, candidateResult.id), - ) + eq(anchor.candidateId, candidateResult.id))) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ candidateResult }) => candidateResult) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1798,14 +1781,12 @@ async function runJoinCell({ keySide === `main` ? q .from({ child: children.collection }) - .innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(tag.id, add(child.tagId, parent.offset)), - ) + .join(innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(tag.id, add(child.tagId, parent.offset)))) : q .from({ child: children.collection }) - .innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(add(tag.id, parent.offset), child.tagId), - ) + .join(innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(add(tag.id, parent.offset), child.tagId))) const routed = ( correlationAttachment === `main` ? joined.where(({ child }) => eq(child.parentGroup, parent.group)) @@ -1923,9 +1904,8 @@ async function runNamespaceCollisionCell({ case `join`: return includeInEveryForm( correlated - .innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(child.id, tag.id), - ) + .join(innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(child.id, tag.id))) .select(({ child }) => ({ id: child.id, value: child.label, @@ -1985,9 +1965,8 @@ async function runNamespaceCollisionCell({ case `join`: return includeInEveryForm( correlated - .innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(child.id, tag.id), - ) + .join(innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(child.id, tag.id))) .select(({ child }) => ({ id: child.id, [name]: child.label, @@ -2158,9 +2137,8 @@ async function runPublicSurfaceCell({ .fn.select(({ candidate }) => candidate.value) const rows = q .from({ anchor: anchors.collection }) - .innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value), - ) + .join(innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value))) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -2229,10 +2207,8 @@ async function runPublicSurfaceCell({ return { id: parent.id, ...includeInEveryForm(rows) } } - const rows = correlated.innerJoin( - { tag: tags.collection }, - ({ child, tag }) => eq(child.id, tag.childId), - ) + const rows = correlated.join(innerJoin({ tag: tags.collection }, + ({ child, tag }) => eq(child.id, tag.childId))) return { id: parent.id, ...includeInEveryForm(rows) } }), ) @@ -2688,9 +2664,8 @@ describe(`compound join parent context`, () => { ) const joined = boundary === `direct` - ? base.innerJoin({ peer: peers.collection }, joinCondition) - : base.innerJoin( - { + ? base.join(innerJoin({ peer: peers.collection }, joinCondition)) + : base.join(innerJoin({ peer: q .from({ peer: peers.collection }) .select(({ peer }) => ({ @@ -2699,8 +2674,7 @@ describe(`compound join parent context`, () => { b: peer.b, })), }, - joinCondition, - ) + joinCondition)) const rows = joined.select(({ child, peer }) => ({ id: child.id, peerId: peer.id, diff --git a/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts b/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts index 46523e423b..80ecf5ce77 100644 --- a/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts +++ b/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts @@ -1,20 +1,19 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, test } from 'vitest' import { Temporal } from 'temporal-polyfill' -import { Func, PropRef, Value } from '../../src/query/ir.js' -import { createCollection } from '../../src/collection/index.js' -import { createFilterFunctionFromExpression } from '../../src/collection/change-events.js' -import { - and, +import { and, count, createLiveQueryCollection, eq, isNull, + leftJoin, lt, not, queryOnce, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' +import { Func, PropRef, Value } from '../../src/query/ir.js' +import { createCollection } from '../../src/collection/index.js' +import { createFilterFunctionFromExpression } from '../../src/collection/change-events.js' import { oraclePropertyOptions, oracleRuns, @@ -541,9 +540,8 @@ function createFlatQuery( query: (q) => q .from({ parent: parents }) - .leftJoin({ child: children }, ({ parent, child }) => - eq(parent.group, child.parentGroup), - ) + .join(leftJoin({ child: children }, ({ parent, child }) => + eq(parent.group, child.parentGroup))) .select(({ parent, child }) => ({ parentId: parent.id, parentGroup: parent.group, diff --git a/packages/db/tests/query/includes-lazy-loading.test.ts b/packages/db/tests/query/includes-lazy-loading.test.ts index f2ed8f5bc3..08716f32fa 100644 --- a/packages/db/tests/query/includes-lazy-loading.test.ts +++ b/packages/db/tests/query/includes-lazy-loading.test.ts @@ -1,11 +1,10 @@ import { describe, expect, it, vi } from 'vitest' -import { - and, +import { and, createLiveQueryCollection, eq, gte, - toArray, -} from '../../src/query/index.js' + leftJoin, + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { extractSimpleComparisons } from '../../src/query/expression-helpers.js' import { @@ -191,9 +190,8 @@ describe(`includes lazy loading`, () => { matches: toArray( q .from({ left: items }) - .join({ right: items }, ({ left, right }) => - eq(left.peerId, right.id), - ) + .join(leftJoin({ right: items }, ({ left, right }) => + eq(left.peerId, right.id))) .where(({ right }) => eq(right.rootId, root.id)) .select(({ left, right }) => ({ left: left.label, diff --git a/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts b/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts index 531b46defa..4350dbc705 100644 --- a/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts +++ b/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts @@ -1,10 +1,9 @@ import { describe, expect, test, vi } from 'vitest' -import { - Query, +import { Query, createLiveQueryCollection, eq, - toArray, -} from '../../src/query/index.js' + joinOf, + toArray } from '../../src/query/index.js' import { BasicIndex } from '../../src/indexes/basic-index.js' import { withHistoryCleanup } from '../optimistic-history-oracle.js' import { flushPromises } from '../utils.js' @@ -153,11 +152,8 @@ describe(`direct outer-join route work`, () => { rows: toArray( new Query() .from({ main: mains.collection }) - .join( - { anchor: anchors.collection }, - ({ main, anchor }) => eq(main.anchorId, anchor.id), - kind, - ) + .join(joinOf(kind, { anchor: anchors.collection }, + ({ main, anchor }) => eq(main.anchorId, anchor.id))) .where(({ main }) => eq(main.parentId, parent.id)) .select(({ main, anchor }) => ({ mainId: main.id, diff --git a/packages/db/tests/query/includes-publication-oracle.test.ts b/packages/db/tests/query/includes-publication-oracle.test.ts index a05fe7406e..74d19fd501 100644 --- a/packages/db/tests/query/includes-publication-oracle.test.ts +++ b/packages/db/tests/query/includes-publication-oracle.test.ts @@ -1,15 +1,14 @@ import { setImmediate as yieldToRunner } from 'node:timers/promises' import { fc, test as fcTest } from '@fast-check/vitest' import { afterEach, describe, expect, it } from 'vitest' +import { createLiveQueryCollection, + eq, + innerJoin, + materialize } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BasicIndex } from '../../src/indexes/basic-index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { createEffect } from '../../src/query/effect.js' -import { - createLiveQueryCollection, - eq, - materialize, -} from '../../src/query/index.js' import { runTrace } from '../trace-runner.js' import { oraclePropertyOptions, readOracleRunConfig } from '../oracle-config.js' import { withHistoryCleanup } from '../optimistic-history-oracle.js' @@ -134,12 +133,9 @@ function createLayeredQuery( const parentRows = q1Shape === `direct` ? source - : source.join( - { metadata: metadata.collection }, + : source.join(innerJoin({ metadata: metadata.collection }, ({ item, metadata: rowMetadata }) => - eq(item.id, rowMetadata.parentId), - `inner`, - ) + eq(item.id, rowMetadata.parentId))) return parentRows.select(({ item }) => ({ item, diff --git a/packages/db/tests/query/includes-query-shape-oracle.test.ts b/packages/db/tests/query/includes-query-shape-oracle.test.ts index 02ad01cc05..7c2e1d9627 100644 --- a/packages/db/tests/query/includes-query-shape-oracle.test.ts +++ b/packages/db/tests/query/includes-query-shape-oracle.test.ts @@ -1,11 +1,10 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect } from 'vitest' -import { BasicIndex } from '../../src/indexes/basic-index.js' -import { - createLiveQueryCollection, +import { createLiveQueryCollection, eq, - materialize, -} from '../../src/query/index.js' + innerJoin, + materialize } from '../../src/query/index.js' +import { BasicIndex } from '../../src/indexes/basic-index.js' import { runTrace } from '../trace-runner.js' import { oraclePropertyOptions, oracleRuns } from '../oracle-config.js' import { createControlledCollection } from './includes-oracle-helpers.js' @@ -125,10 +124,8 @@ function createMultiplicityQuery(sources: MultiplicitySources) { query: (q) => q .from({ parent: sources.parents.collection }) - .innerJoin( - { child: sources.children.collection }, - ({ parent, child }) => eq(parent.id, child.parentId), - ) + .join(innerJoin({ child: sources.children.collection }, + ({ parent, child }) => eq(parent.id, child.parentId))) .select(({ parent }) => ({ id: parent.id })), getKey: (row) => row.id, }) @@ -231,10 +228,8 @@ function createCorrelationQuery( .select(({ part }) => { const joined = q .from({ production: sources.productions.collection }) - .innerJoin( - { order: sources.orders.collection }, - ({ production, order }) => eq(production.orderId, order.id), - ) + .join(innerJoin({ order: sources.orders.collection }, + ({ production, order }) => eq(production.orderId, order.id))) const correlated = target === `joined` ? joined.where(({ order }) => eq(order.partId, part.id)) diff --git a/packages/db/tests/query/includes-scope-identity-oracle.ts b/packages/db/tests/query/includes-scope-identity-oracle.ts index 61b740c0c1..1fb8e8318d 100644 --- a/packages/db/tests/query/includes-scope-identity-oracle.ts +++ b/packages/db/tests/query/includes-scope-identity-oracle.ts @@ -1,14 +1,13 @@ import { fc } from '@fast-check/vitest' -import { createCollection } from '../../src/collection/index.js' -import { createFilterFunctionFromExpression } from '../../src/collection/change-events.js' -import { - and, - createLiveQueryCollection, - eq, +import { and, createLiveQueryCollection, eq , + innerJoin, + joinOf, + leftJoin, materialize, not, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' +import { createCollection } from '../../src/collection/index.js' +import { createFilterFunctionFromExpression } from '../../src/collection/change-events.js' import { mockSyncCollectionOptions } from '../utils.js' import type { Collection } from '../../src/collection/index.js' import type { LoadSubsetOptions } from '../../src/types.js' @@ -614,9 +613,8 @@ export function createScopedQuery( branch(n.activeBranch!, true), branch(n.inactiveBranch!, false), ) - .innerJoin({ [n.anchor!]: sources.parts.collection }, (c: Context) => - eq(c.id, c[n.anchor!].id), - ) + .join(innerJoin({ [n.anchor!]: sources.parts.collection }, (c: Context) => + eq(c.id, c[n.anchor!].id))) .select((c: Context) => ({ id: c.id, active: c.active, @@ -647,11 +645,8 @@ export function createScopedQuery( [n.wrapped!]: q.from({ [n.wrapped!]: sources.parts.collection }), } as any) as any ) - .join( - { [n.joinSub!]: clientOne }, - (c: Context) => eq(c[n.joinSub!].partId, c[n.wrapped!].id), - shape.wrapperJoin, - ) + .join(joinOf(shape.wrapperJoin, { [n.joinSub!]: clientOne }, + (c: Context) => eq(c[n.joinSub!].partId, c[n.wrapped!].id))) .select((c: Context) => ({ id: c[n.wrapped!].id, active: c[n.wrapped!].active, @@ -662,18 +657,12 @@ export function createScopedQuery( const { subquery, include } = shape let sub: any = q.from({ [n.sub!]: sources.parts.collection }) if (subquery.joins !== `none`) { - sub = sub.join( - { [n.joined!]: sources.refs.collection }, - (c: Context) => eq(c[n.joined!].partId, c[n.sub!].id), - subquery.refsJoin, - ) + sub = sub.join(joinOf(subquery.refsJoin, { [n.joined!]: sources.refs.collection }, + (c: Context) => eq(c[n.joined!].partId, c[n.sub!].id))) } if (subquery.joins === `refsThenNotes`) { - sub = sub.join( - { [n.noted!]: sources.notes.collection }, - (c: Context) => eq(c[n.noted!].partId, c[n.sub!].id), - `left`, - ) + sub = sub.join(leftJoin({ [n.noted!]: sources.notes.collection }, + (c: Context) => eq(c[n.noted!].partId, c[n.sub!].id))) } const predicates = subquery.predicates.map((predicate) => predicate === `subActive` @@ -729,10 +718,8 @@ export function createScopedQuery( branch(n.include!, clientOne), branch(n.includeOther!, (member) => not(clientOne(member))), ) - .innerJoin( - { [n.includeAnchor!]: includeCollection }, - (cc: Context) => eq(cc.memberId, cc[n.includeAnchor!].id), - ) + .join(innerJoin({ [n.includeAnchor!]: includeCollection }, + (cc: Context) => eq(cc.memberId, cc[n.includeAnchor!].id))) .where((cc: Context) => eq(cc[n.includeAnchor!].partId, outer.id)) if (include.clientFilter) { child = child.where((cc: Context) => @@ -761,11 +748,8 @@ export function createScopedQuery( }) : q.from({ [member]: includeCollection }) if (include.body === `joined`) { - child = child.join( - { [n.includeJoin!]: sources.parts.collection }, - (cc: Context) => eq(cc[n.includeJoin!].id, cc[member].partId), - `inner`, - ) + child = child.join(innerJoin({ [n.includeJoin!]: sources.parts.collection }, + (cc: Context) => eq(cc[n.includeJoin!].id, cc[member].partId))) } child = child.where((cc: Context) => eq(cc[member].partId, outer.id)) if (include.clientFilter) { diff --git a/packages/db/tests/query/includes-temporal-oracle.test.ts b/packages/db/tests/query/includes-temporal-oracle.test.ts index f146360acd..2d877d163c 100644 --- a/packages/db/tests/query/includes-temporal-oracle.test.ts +++ b/packages/db/tests/query/includes-temporal-oracle.test.ts @@ -1,5 +1,11 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, it, vi } from 'vitest' +import { createLiveQueryCollection, + eq, + leftJoin, + materialize, + queryOnce, + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { BasicIndex } from '../../src/indexes/basic-index.js' @@ -9,13 +15,6 @@ import { SubsetDemandController } from '../../src/query/live/subset-demand-contr import { DeduplicatedLoadSubset } from '../../src/query/subset-dedupe.js' import { createEffect } from '../../src/query/effect.js' import { LoadSubsetOperationAbortedError } from '../../src/errors.js' -import { - createLiveQueryCollection, - eq, - materialize, - queryOnce, - toArray, -} from '../../src/query/index.js' import { runTrace } from '../trace-runner.js' import { oraclePropertyOptions, @@ -2985,7 +2984,7 @@ function laggingSourceQuery( })) return q .from({ user: users! }) - .join({ post: posts }, ({ user, post }) => eq(user.id, post.userId)) + .join(leftJoin({ post: posts }, ({ user, post }) => eq(user.id, post.userId))) .select(({ user, post }) => ({ id: user.id, postId: post.id })) } } diff --git a/packages/db/tests/query/includes-work-counter-oracle.test.ts b/packages/db/tests/query/includes-work-counter-oracle.test.ts index 1296cb0f86..16a33fb9c2 100644 --- a/packages/db/tests/query/includes-work-counter-oracle.test.ts +++ b/packages/db/tests/query/includes-work-counter-oracle.test.ts @@ -1,16 +1,15 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { beforeAll, describe, expect, it } from 'vitest' +import { createLiveQueryCollection, eq, + inArray, + innerJoin, + leftJoin, + materialize, + toArray } from '../../src/query/index.js' import { oraclePropertyOptions, oracleRuns } from '../oracle-config.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { localOnlyCollectionOptions } from '../../src/local-only.js' -import { - createLiveQueryCollection, - eq, - inArray, - materialize, - toArray, -} from '../../src/query/index.js' import { evaluateReferenceExpression } from '../reference-expression-oracle.js' import { SetWindowFindOneLimitError, @@ -322,11 +321,9 @@ async function observeWork({ links: joinTargets ? materialize( selectedLinks - .innerJoin( - { target: sources.terms }, + .join(innerJoin({ target: sources.terms }, ({ link, target }) => - eq(link.targetId, target.id), - ) + eq(link.targetId, target.id))) .select(({ link, target }) => ({ id: link.id, text: target.text, @@ -2080,7 +2077,7 @@ describe(`static include demand and setWindow ownership`, () => { ? q.from({ page }) : q .from({ row: rows }) - .join({ page }, ({ row, page: paged }) => eq(row.id, paged.id)) + .join(leftJoin({ page }, ({ row, page: paged }) => eq(row.id, paged.id))) }) try { await live.preload() diff --git a/packages/db/tests/query/includes.test.ts b/packages/db/tests/query/includes.test.ts index b4a9def149..81bdc135fa 100644 --- a/packages/db/tests/query/includes.test.ts +++ b/packages/db/tests/query/includes.test.ts @@ -1,15 +1,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - and, +import { and, coalesce, concat, count, createLiveQueryCollection, eq, gte, + innerJoin, materialize, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { localOnlyCollectionOptions } from '../../src/local-only.js' @@ -3458,12 +3457,9 @@ describe(`includes subqueries`, () => { joinedChildren: materialize( q .from({ joinedChild: children }) - .join( - { metadata: metadataRows }, + .join(innerJoin({ metadata: metadataRows }, ({ joinedChild, metadata }) => - eq(joinedChild.metadataId, metadata.id), - `inner`, - ) + eq(joinedChild.metadataId, metadata.id))) .where(({ metadata }) => eq(metadata.groupId, parent.groupId)) .where(({ metadata }) => eq(metadata.tag, parent.tag)) .select(({ joinedChild }) => ({ id: joinedChild.id })), diff --git a/packages/db/tests/query/indexes.test.ts b/packages/db/tests/query/indexes.test.ts index eb7f4f17a5..4517e3ec51 100644 --- a/packages/db/tests/query/indexes.test.ts +++ b/packages/db/tests/query/indexes.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest' +import { innerJoin, leftJoin, rightJoin } from '@tanstack/db' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index' @@ -389,9 +390,8 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join({ other: secondCollection }, ({ item, other }: any) => - eq(item.status, other.status), - ) + .join(leftJoin({ other: secondCollection }, ({ item, other }: any) => + eq(item.status, other.status))) .where(({ item }: any) => eq(item.age, 25)) .select(({ item, other }: any) => ({ id: item.id, @@ -499,9 +499,8 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join({ other: secondCollection }, ({ item, other }: any) => - eq(item.id, other.id), - ) + .join(leftJoin({ other: secondCollection }, ({ item, other }: any) => + eq(item.id, other.id))) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -603,11 +602,8 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join( - { other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2), - `inner`, - ) + .join(innerJoin({ other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2))) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -703,11 +699,8 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join( - { other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2), - `inner`, - ) + .join(innerJoin({ other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2))) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -798,11 +791,8 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join( - { other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2), - `left`, - ) + .join(leftJoin({ other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2))) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -918,11 +908,8 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join( - { other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2), - `left`, - ) + .join(leftJoin({ other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2))) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -1016,11 +1003,8 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join( - { other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2), - `right`, - ) + .join(rightJoin({ other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2))) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -1110,11 +1094,8 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join( - { other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2), - `right`, - ) + .join(rightJoin({ other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2))) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) diff --git a/packages/db/tests/query/ir-stable-identity-oracle.test.ts b/packages/db/tests/query/ir-stable-identity-oracle.test.ts index f475760351..0410892357 100644 --- a/packages/db/tests/query/ir-stable-identity-oracle.test.ts +++ b/packages/db/tests/query/ir-stable-identity-oracle.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it, vi } from 'vitest' import { fc, test as fcTest } from '@fast-check/vitest' import { Temporal } from 'temporal-polyfill' +import { innerJoin, leftJoin } from '@tanstack/db' +import { processJoins } from '../../src/query/compiler/joins.js' +import { applyOptimizations } from '../../src/query/optimizer-joins.js' import { CollectionImpl } from '../../src/collection/index.js' import { Query, getQueryIR } from '../../src/query/builder/index.js' import { @@ -868,7 +871,7 @@ function createAlphaRenamedJoinQuery( userAlias, ), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `inner`, from: new CollectionRef( postsCollection as unknown as ConstructorParameters< @@ -910,7 +913,7 @@ function createAlphaRenamedImplicitJoinQuery( userAlias, ), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `inner`, from: new CollectionRef( postsCollection as unknown as ConstructorParameters< @@ -1273,11 +1276,8 @@ const structuredQueries: Array<[string, () => QueryIR]> = [ getQueryIR( new Query() .from({ user: usersCollection }) - .join( - { post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId), - `left`, - ) + .join(leftJoin({ post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId))) .where(({ post }) => eq(post.published, true)) .select(({ user, post }) => ({ userId: user.id, @@ -1295,15 +1295,12 @@ const structuredQueries: Array<[string, () => QueryIR]> = [ .from({ post: postsCollection }) .where(({ post }) => gt(post.views, 100)), }) - .join( - { + .join(innerJoin({ activeUser: new Query() .from({ user: usersCollection }) .where(({ user }) => eq(user.status, `active`)), }, - ({ post, activeUser }) => eq(post.userId, activeUser.id), - `inner`, - ), + ({ post, activeUser }) => eq(post.userId, activeUser.id))), ), ], [ diff --git a/packages/db/tests/query/join-result-key-oracle.property.test.ts b/packages/db/tests/query/join-result-key-oracle.property.test.ts index 7fcfafc4e3..092a3d55df 100644 --- a/packages/db/tests/query/join-result-key-oracle.property.test.ts +++ b/packages/db/tests/query/join-result-key-oracle.property.test.ts @@ -42,8 +42,8 @@ */ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, it } from 'vitest' +import { createLiveQueryCollection, eq, joinOf } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' -import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { oraclePropertyOptions, oracleRuns, @@ -224,7 +224,7 @@ async function runHistory(history: History): Promise { const live = createLiveQueryCollection((q) => q .from({ l: left }) - .join({ r: right }, ({ l, r }) => eq(l.g, r.g), history.join) + .join(joinOf(history.join, { r: right }, ({ l, r }) => eq(l.g, r.g))) .select(({ l, r }) => ({ l: l.id, r: r.id })), ) const check = (checkpoint: string) => { diff --git a/packages/db/tests/query/join-subquery.test-d.ts b/packages/db/tests/query/join-subquery.test-d.ts index 070584a7a9..0d88ec6574 100644 --- a/packages/db/tests/query/join-subquery.test-d.ts +++ b/packages/db/tests/query/join-subquery.test-d.ts @@ -1,5 +1,5 @@ import { describe, expectTypeOf, test } from 'vitest' -import { createLiveQueryCollection, eq, gt } from '../../src/query/index.js' +import { createLiveQueryCollection, eq, gt, innerJoin, leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -99,11 +99,8 @@ describe(`Join Subquery Types`, () => { // Join subquery with users return q .from({ issue: project1Issues }) - .join( - { user: usersCollection }, - ({ issue, user }) => eq(issue.userId, user.id), - `inner`, - ) + .join(innerJoin({ user: usersCollection }, + ({ issue, user }) => eq(issue.userId, user.id))) .select(({ issue, user }) => ({ issue_title: issue.title, user_name: user.name, @@ -138,11 +135,8 @@ describe(`Join Subquery Types`, () => { // Join all issues with active users subquery - no SELECT to test namespaced result return q .from({ issue: issuesCollection }) - .join( - { activeUser: activeUsers }, - ({ issue, activeUser }) => eq(issue.userId, activeUser.id), - `left`, - ) + .join(leftJoin({ activeUser: activeUsers }, + ({ issue, activeUser }) => eq(issue.userId, activeUser.id))) }, }) @@ -174,12 +168,9 @@ describe(`Join Subquery Types`, () => { // Join both subqueries return q .from({ longIssue: longIssues }) - .join( - { activeUser: activeUsers }, + .join(innerJoin({ activeUser: activeUsers }, ({ longIssue, activeUser }) => - eq(longIssue.userId, activeUser.id), - `inner`, - ) + eq(longIssue.userId, activeUser.id))) .select(({ longIssue, activeUser }) => ({ issue_title: longIssue.title, issue_duration: longIssue.duration, @@ -215,11 +206,8 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: issuesCollection }) - .join( - { engUser: engineeringUsers }, - ({ issue, engUser }) => eq(issue.userId, engUser.id), - `inner`, - ) + .join(innerJoin({ engUser: engineeringUsers }, + ({ issue, engUser }) => eq(issue.userId, engUser.id))) .select(({ issue, engUser }) => ({ issue_title: issue.title, user_name: engUser.name, @@ -251,11 +239,8 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: issuesCollection }) - .join( - { activeUser: activeUsers }, - ({ issue, activeUser }) => eq(issue.userId, activeUser.id), - `left`, - ) + .join(leftJoin({ activeUser: activeUsers }, + ({ issue, activeUser }) => eq(issue.userId, activeUser.id))) }, }) @@ -300,11 +285,8 @@ describe(`Join Subquery Types`, () => { // Join both transformed subqueries return q .from({ task: transformedIssues }) - .join( - { profile: userProfiles }, - ({ task, profile }) => eq(task.assigneeId, profile.profileId), - `inner`, - ) + .join(innerJoin({ profile: userProfiles }, + ({ task, profile }) => eq(task.assigneeId, profile.profileId))) .select(({ task, profile }) => ({ id: task.taskId, name: task.taskName, @@ -346,11 +328,8 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: filteredIssues }) - .join( - { user: usersCollection }, - ({ issue, user }) => eq(issue.userId, user.id), - `inner`, - ) + .join(innerJoin({ user: usersCollection }, + ({ issue, user }) => eq(issue.userId, user.id))) .select(({ issue, user }) => ({ // Should have access to all original Issue properties issue_id: issue.id, @@ -394,11 +373,8 @@ describe(`Join Subquery Types`, () => { // Join all issues with active users subquery with SELECT return q .from({ issue: issuesCollection }) - .join( - { activeUser: activeUsers }, - ({ issue, activeUser }) => eq(issue.userId, activeUser.id), - `left`, - ) + .join(leftJoin({ activeUser: activeUsers }, + ({ issue, activeUser }) => eq(issue.userId, activeUser.id))) .select(({ issue, activeUser }) => ({ issue_title: issue.title, user_name: activeUser.name, // Should now be string | undefined diff --git a/packages/db/tests/query/join-subquery.test.ts b/packages/db/tests/query/join-subquery.test.ts index a0f4b6e779..61139f646c 100644 --- a/packages/db/tests/query/join-subquery.test.ts +++ b/packages/db/tests/query/join-subquery.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { createLiveQueryCollection, eq, gt } from '../../src/query/index.js' +import { createLiveQueryCollection, eq, gt, innerJoin , joinOf, leftJoin, rightJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' @@ -223,11 +223,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Join subquery with users return q .from({ issue: project1Issues }) - .join( - { user: usersCollection }, - ({ issue, user }) => eq(issue.userId, user.id), - `inner`, - ) + .join(innerJoin({ user: usersCollection }, + ({ issue, user }) => eq(issue.userId, user.id))) .select(({ issue, user }) => ({ issue_title: issue.title, user_name: user.name, @@ -262,11 +259,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Join all issues with active users subquery return q .from({ issue: issuesCollection }) - .join( - { activeUser: activeUsers }, - ({ issue, activeUser }) => eq(issue.userId, activeUser.id), - `left`, - ) + .join(leftJoin({ activeUser: activeUsers }, + ({ issue, activeUser }) => eq(issue.userId, activeUser.id))) .select(({ issue, activeUser }) => ({ issue_title: issue.title, user_name: activeUser.name, @@ -310,12 +304,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Join both subqueries return q .from({ longIssue: longIssues }) - .join( - { activeUser: activeUsers }, + .join(innerJoin({ activeUser: activeUsers }, ({ longIssue, activeUser }) => - eq(longIssue.userId, activeUser.id), - `inner`, - ) + eq(longIssue.userId, activeUser.id))) .select(({ longIssue, activeUser }) => ({ issue_title: longIssue.title, issue_duration: longIssue.duration, @@ -371,11 +362,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .join( - { engUser: engineeringUsers }, - ({ issue, engUser }) => eq(issue.userId, engUser.id), - `inner`, - ) + .join(innerJoin({ engUser: engineeringUsers }, + ({ issue, engUser }) => eq(issue.userId, engUser.id))) .select(({ issue, engUser }) => ({ issue_title: issue.title, user_name: engUser.name, @@ -414,22 +402,16 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .join( - { user: activeUsers }, - ({ issue, user }) => eq(issue.userId, user.id), - joinType, - ) + .join(joinOf(joinType, { user: activeUsers }, + ({ issue, user }) => eq(issue.userId, user.id))) .where(({ user }) => eq(user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) } return q .from({ issue: issuesCollection }) - .join( - { user: usersCollection }, - ({ issue, user }) => eq(issue.userId, user.id), - joinType, - ) + .join(joinOf(joinType, { user: usersCollection }, + ({ issue, user }) => eq(issue.userId, user.id))) .where(({ user }) => eq(user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -449,9 +431,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .innerJoin({ user: activeUsers }, ({ issue, user }) => - eq(issue.userId, user.id), - ) + .join(innerJoin({ user: activeUsers }, ({ issue, user }) => + eq(issue.userId, user.id))) .where(({ user }) => eq(user.name, `Charlie`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -470,9 +451,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .innerJoin({ member: activeUsers }, ({ issue, member }) => - eq(issue.userId, member.id), - ) + .join(innerJoin({ member: activeUsers }, ({ issue, member }) => + eq(issue.userId, member.id))) .where(({ member }) => eq(member.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -487,15 +467,13 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { query: (q) => { const usersWithProfiles = q .from({ user: usersCollection }) - .innerJoin({ profile: profilesCollection }, ({ user, profile }) => - eq(user.id, profile.userId), - ) + .join(innerJoin({ profile: profilesCollection }, ({ user, profile }) => + eq(user.id, profile.userId))) return q .from({ issue: issuesCollection }) - .innerJoin({ member: usersWithProfiles }, ({ issue, member }) => - eq(issue.userId, member.user.id), - ) + .join(innerJoin({ member: usersWithProfiles }, ({ issue, member }) => + eq(issue.userId, member.user.id))) .where(({ member }) => eq(member.user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -510,16 +488,14 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { query: (q) => { const usersWithProfiles = q .from({ user: usersCollection }) - .innerJoin({ profile: profilesCollection }, ({ user, profile }) => - eq(user.id, profile.userId), - ) + .join(innerJoin({ profile: profilesCollection }, ({ user, profile }) => + eq(user.id, profile.userId))) .select(({ user }) => user) return q .from({ issue: issuesCollection }) - .innerJoin({ member: usersWithProfiles }, ({ issue, member }) => - eq(issue.userId, member.id), - ) + .join(innerJoin({ member: usersWithProfiles }, ({ issue, member }) => + eq(issue.userId, member.id))) .where(({ member }) => eq(member.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -539,11 +515,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .join( - { activeUser: activeUsers }, - ({ issue, activeUser }) => eq(issue.userId, activeUser.id), - `left`, - ) + .join(leftJoin({ activeUser: activeUsers }, + ({ issue, activeUser }) => eq(issue.userId, activeUser.id))) .select(({ issue, activeUser }) => ({ issue_title: issue.title, issue_status: issue.status, @@ -577,15 +550,12 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { query: (q) => { return q .from({ product: productsCollection }) - .join( - { + .join(leftJoin({ tried: q .from({ tried: trialsCollection }) .where(({ tried }) => eq(tried.userId, 1)), }, - ({ tried, product }) => eq(tried.productId, product.id), - `left`, - ) + ({ tried, product }) => eq(tried.productId, product.id))) .where(({ product }) => eq(product.id, 1)) .select(({ product, tried }) => ({ product, @@ -615,17 +585,14 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { query: (q) => { return q .from({ issue: issuesCollection }) - .join( - { + .join(leftJoin({ users: q .from({ user: usersCollection }) .where(({ user }) => eq(user.status, `active`)) .orderBy(({ user }) => user.name, `asc`) .limit(1), }, - ({ issue, users }) => eq(issue.userId, users.id), - `left`, - ) + ({ issue, users }) => eq(issue.userId, users.id))) .orderBy(({ issue }) => issue.id, `desc`) .limit(1) }, @@ -665,11 +632,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { .orderBy(({ user }) => user.name, `asc`) .limit(1), }) - .join( - { issue: issuesCollection }, - ({ issue, users }) => eq(issue.userId, users.id), - `right`, - ) + .join(rightJoin({ issue: issuesCollection }, + ({ issue, users }) => eq(issue.userId, users.id))) .orderBy(({ issue }) => issue.id, `desc`) .limit(1) }, @@ -727,11 +691,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Join both transformed subqueries return q .from({ task: transformedIssues }) - .join( - { profile: userProfiles }, - ({ task, profile }) => eq(task.assigneeId, profile.profileId), - `inner`, - ) + .join(innerJoin({ profile: userProfiles }, + ({ task, profile }) => eq(task.assigneeId, profile.profileId))) .select(({ task, profile }) => ({ id: task.taskId, name: task.taskName, @@ -806,9 +767,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ user: activeUsers }) - .innerJoin({ issue: issuesCollection }, ({ user, issue }) => - eq(user.id, issue.userId), - ) + .join(innerJoin({ issue: issuesCollection }, ({ user, issue }) => + eq(user.id, issue.userId))) .where(({ user }) => eq(user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -843,9 +803,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ row: allIssues }) - .innerJoin({ user: usersCollection }, ({ row, user }) => - eq(row.userId, user.id), - ) + .join(innerJoin({ user: usersCollection }, ({ row, user }) => + eq(row.userId, user.id))) .where(({ row }) => eq(row.status, `open`)) .select(({ row }) => ({ id: row.id })) }, @@ -874,11 +833,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Filter for active users at the subquery level to avoid WHERE on SELECT fields bug const activeUsersWithProfiles = q .from({ user: usersCollection }) - .join( - { profile: profilesCollection }, - ({ user, profile }) => eq(user.id, profile.userId), - `inner`, - ) + .join(innerJoin({ profile: profilesCollection }, + ({ user, profile }) => eq(user.id, profile.userId))) .where(({ user }) => eq(user.status, `active`)) .select(({ user, profile }) => ({ userId: user.id, @@ -893,11 +849,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // This tests that aliasRemapping['activeUser'] = 'user' (flattened to innermost) return q .from({ activeUser: activeUsersWithProfiles }) - .join( - { issue: issuesCollection }, - ({ activeUser, issue }) => eq(issue.userId, activeUser.userId), - `inner`, - ) + .join(innerJoin({ issue: issuesCollection }, + ({ activeUser, issue }) => eq(issue.userId, activeUser.userId))) .select(({ activeUser, issue }) => ({ issue_title: issue.title, issue_status: issue.status, @@ -957,11 +910,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Level 1: Subquery WITH a join (user + profile) const usersWithProfiles = q .from({ user: usersCollection }) - .join( - { profile: profilesCollection }, - ({ user, profile }) => eq(user.id, profile.userId), - `inner`, - ) + .join(innerJoin({ profile: profilesCollection }, + ({ user, profile }) => eq(user.id, profile.userId))) .where(({ user }) => eq(user.status, `active`)) .select(({ user, profile }) => ({ userId: user.id, @@ -974,11 +924,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // This tests that aliasRemapping['author'] = 'user' for lazy loading return q .from({ issue: issuesCollection }) - .join( - { author: usersWithProfiles }, - ({ issue, author }) => eq(issue.userId, author.userId), - `left`, - ) + .join(leftJoin({ author: usersWithProfiles }, + ({ issue, author }) => eq(issue.userId, author.userId))) .select(({ issue, author }) => ({ issue_id: issue.id, issue_title: issue.title, @@ -1011,11 +958,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Level 1: Base joined subquery (user + profile) const usersWithProfiles = q .from({ user: usersCollection }) - .join( - { profile: profilesCollection }, - ({ user, profile }) => eq(user.id, profile.userId), - `inner`, - ) + .join(innerJoin({ profile: profilesCollection }, + ({ user, profile }) => eq(user.id, profile.userId))) .select(({ user, profile }) => ({ userId: user.id, userName: user.name, @@ -1038,11 +982,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Tests that aliasRemapping['author'] = 'user' (flattened to innermost, not 'userProfile') return q .from({ issue: issuesCollection }) - .join( - { author: activeUsersWithProfiles }, - ({ issue, author }) => eq(issue.userId, author.id), - `inner`, - ) + .join(innerJoin({ author: activeUsersWithProfiles }, + ({ issue, author }) => eq(issue.userId, author.id))) .select(({ issue, author }) => ({ issue_title: issue.title, author_name: author.name, @@ -1096,9 +1037,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { const right = shared ? left : activeUsers() let query = q .from({ leftUser: left }) - .innerJoin({ rightUser: right }, ({ leftUser, rightUser }) => - eq(leftUser.id, rightUser.id), - ) + .join(innerJoin({ rightUser: right }, ({ leftUser, rightUser }) => + eq(leftUser.id, rightUser.id))) if (filterRight) { query = query.where(({ rightUser }) => @@ -1173,16 +1113,14 @@ describe(`Lazy join: subquery whose join key resolves to an indexed collection`, query: (q) => { const teamByMember = q .from({ member: members }) - .leftJoin({ team: teams }, ({ team, member }) => - eq(team.id, member.teamId), - ) + .join(leftJoin({ team: teams }, ({ team, member }) => + eq(team.id, member.teamId))) .select(({ team }) => ({ teamId: team.id })) return q .from({ m: members }) - .leftJoin({ memberTeam: teamByMember }, ({ m, memberTeam }) => - eq(memberTeam.teamId, m.teamId), - ) + .join(leftJoin({ memberTeam: teamByMember }, ({ m, memberTeam }) => + eq(memberTeam.teamId, m.teamId))) .select(({ m }) => ({ id: m.id })) }, }) @@ -1239,9 +1177,8 @@ describe(`Lazy join index availability`, () => { const live = createLiveQueryCollection((q) => q .from({ team: teams }) - .leftJoin({ member: members }, ({ team, member }) => - eq(team.id, member.teamId), - ) + .join(leftJoin({ member: members }, ({ team, member }) => + eq(team.id, member.teamId))) .select(({ team, member }) => ({ id: team.id, memberId: member.id, @@ -1295,9 +1232,8 @@ describe(`Lazy join index availability`, () => { const live = createLiveQueryCollection((q) => q .from({ team: teams }) - .leftJoin({ member: members }, ({ team, member }) => - eq(team.id, member.teamId), - ) + .join(leftJoin({ member: members }, ({ team, member }) => + eq(team.id, member.teamId))) .select(({ team, member }) => ({ id: team.id, memberId: member.id, diff --git a/packages/db/tests/query/join.test-d.ts b/packages/db/tests/query/join.test-d.ts index b8cf6e5176..8320187c9d 100644 --- a/packages/db/tests/query/join.test-d.ts +++ b/packages/db/tests/query/join.test-d.ts @@ -1,7 +1,7 @@ import { describe, expectTypeOf, test } from 'vitest' import { z } from 'zod' import { type } from 'arktype' -import { createLiveQueryCollection, eq } from '../../src/query/index.js' +import { createLiveQueryCollection, eq, fullJoin, innerJoin, leftJoin, rightJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, omitVirtualProps } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -52,11 +52,8 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `inner`, - ), + .join(innerJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))), }) const results = innerJoinQuery.toArray @@ -80,11 +77,8 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `left`, - ), + .join(leftJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))), }) const results = leftJoinQuery.toArray @@ -108,11 +102,8 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `right`, - ), + .join(rightJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))), }) const results = rightJoinQuery.toArray @@ -136,11 +127,8 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `full`, - ), + .join(fullJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))), }) const results = fullJoinQuery.toArray @@ -179,16 +167,10 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `left`, // dept is optional - ) - .join( - { project: projectsCollection }, - ({ user, project }) => eq(user.id, project.user_id), - `right`, // user becomes optional, project required - ), + .join(leftJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) + .join(rightJoin({ project: projectsCollection }, + ({ user, project }) => eq(user.id, project.user_id))), }) const results = multipleJoinQuery.toArray @@ -216,11 +198,8 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `left`, - ) + .join(leftJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, // This should still be accessible in select @@ -252,9 +231,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))), }) const results = leftJoinQuery.toArray @@ -278,9 +256,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .rightJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join(rightJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))), }) const results = rightJoinQuery.toArray @@ -304,9 +281,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join(innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))), }) const results = innerJoinQuery.toArray @@ -330,9 +306,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .fullJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join(fullJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))), }) const results = fullJoinQuery.toArray @@ -371,12 +346,10 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) - .rightJoin({ project: projectsCollection }, ({ user, project }) => - eq(user.id, project.user_id), - ), + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) + .join(rightJoin({ project: projectsCollection }, ({ user, project }) => + eq(user.id, project.user_id))), }) const results = multipleJoinQuery.toArray @@ -404,9 +377,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, // This should be string | undefined due to left join @@ -436,9 +408,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, // This should be string (not undefined) due to inner join @@ -482,14 +453,10 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) - .join( - { project: projectsCollection }, - ({ dept, project }) => eq(dept.id, project.department_id), - `inner`, - ), + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) + .join(innerJoin({ project: projectsCollection }, + ({ dept, project }) => eq(dept.id, project.department_id))), }) const results = mixedJoinQuery.toArray @@ -543,10 +510,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin( - { user: userCollection }, - ({ event, user }) => eq(event.user_id, user.id), // Should work with optional field - ) + .join(innerJoin({ user: userCollection }, + ({ event, user }) => eq(event.user_id, user.id))) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -558,10 +523,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin( - { user: userCollection }, - ({ event, user }) => eq(user.id, event.user_id), // Swapped argument order - ) + .join(innerJoin({ user: userCollection }, + ({ event, user }) => eq(user.id, event.user_id))) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -632,10 +595,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin( - { user: userCollection }, - ({ event, user }) => eq(event.user_id, user.id), // Should work with optional field - ) + .join(innerJoin({ user: userCollection }, + ({ event, user }) => eq(event.user_id, user.id))) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -647,10 +608,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin( - { user: userCollection }, - ({ event, user }) => eq(user.id, event.user_id), // Swapped argument order - ) + .join(innerJoin({ user: userCollection }, + ({ event, user }) => eq(user.id, event.user_id))) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -721,10 +680,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ post: postCollection }) - .leftJoin( - { user: userCollection }, - ({ post, user }) => eq(post.author_id, user.id), // Should work with nullable field - ) + .join(leftJoin({ user: userCollection }, + ({ post, user }) => eq(post.author_id, user.id))) .select(({ post, user }) => ({ postTitle: post.title, authorName: user.name, // This will be string | undefined due to left join @@ -752,9 +709,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ userName: user.name, // dept is a proxy ref that is always present at build time, @@ -787,9 +743,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .rightJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(rightJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ // user is the nullable side in a right join userName: user.name, @@ -817,9 +772,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .fullJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(fullJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, @@ -847,9 +801,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, @@ -881,9 +834,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ userName: user.name, // dept is a proxy ref that is always present at build time, @@ -914,9 +866,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .rightJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(rightJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ // user is the nullable side in a right join userName: user.name, @@ -942,9 +893,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .fullJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(fullJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, @@ -970,9 +920,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, @@ -1036,10 +985,8 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin( - { user: userCollection }, - ({ event, user }) => eq(event.user_id, user.id), // Should work with optional field - ) + .join(innerJoin({ user: userCollection }, + ({ event, user }) => eq(event.user_id, user.id))) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -1051,10 +998,8 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin( - { user: userCollection }, - ({ event, user }) => eq(user.id, event.user_id), // Swapped argument order - ) + .join(innerJoin({ user: userCollection }, + ({ event, user }) => eq(user.id, event.user_id))) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -1125,10 +1070,8 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ post: postCollection }) - .leftJoin( - { user: userCollection }, - ({ post, user }) => eq(post.author_id, user.id), // Should work with nullable field - ) + .join(leftJoin({ user: userCollection }, + ({ post, user }) => eq(post.author_id, user.id))) .select(({ post, user }) => ({ postTitle: post.title, authorName: user.name, // This will be string | undefined due to left join @@ -1192,9 +1135,8 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ post: postCollection }) - .innerJoin({ user: userCollection }, ({ post, user }) => - eq(post.user_id, user.id), - ) + .join(innerJoin({ user: userCollection }, ({ post, user }) => + eq(post.user_id, user.id))) .select(({ post, user }) => ({ postTitle: post.title, userName: user.name, diff --git a/packages/db/tests/query/join.test.ts b/packages/db/tests/query/join.test.ts index c08f56ac38..5c65fbacab 100644 --- a/packages/db/tests/query/join.test.ts +++ b/packages/db/tests/query/join.test.ts @@ -1,17 +1,16 @@ import { beforeEach, describe, expect, test } from 'vitest' import { Temporal } from 'temporal-polyfill' -import { - concat, - createLiveQueryCollection, - eq, - gt, +import { concat, createLiveQueryCollection, eq, fullJoin, gt , inArray, + innerJoin, isNull, isUndefined, + joinOf, + leftJoin, lt, not, or, -} from '../../src/query/index.js' + rightJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { flushPromises, @@ -119,11 +118,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - joinType, - ) + .join(joinOf(joinType, { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -207,11 +203,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - joinType, - ), + .join(joinOf(joinType, { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))), }) const results = joinQuery.toArray as Array< @@ -300,11 +293,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - joinType, - ) + .join(joinOf(joinType, { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -343,11 +333,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - joinType, - ) + .join(joinOf(joinType, { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -382,11 +369,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - joinType, - ) + .join(joinOf(joinType, { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -431,11 +415,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - joinType, - ) + .join(joinOf(joinType, { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -534,9 +515,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .leftJoin({ member: teamMembersCollection }, ({ team, member }) => - eq(team.id, member.team_id), - ) + .join(leftJoin({ member: teamMembersCollection }, ({ team, member }) => + eq(team.id, member.team_id))) .where(({ member }) => eq(member.user_id, 100)) .select(({ team, member }) => ({ team_id: team.id, @@ -553,10 +533,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .rightJoin( - { member: teamMembersCollection }, - ({ team, member }) => eq(team.id, member.team_id), - ) + .join(rightJoin({ member: teamMembersCollection }, + ({ team, member }) => eq(team.id, member.team_id))) .where(({ team }) => eq(team.active, true)) .select(({ team, member }) => ({ team_id: team.id, @@ -573,9 +551,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .fullJoin({ member: teamMembersCollection }, ({ team, member }) => - eq(team.id, member.team_id), - ) + .join(fullJoin({ member: teamMembersCollection }, ({ team, member }) => + eq(team.id, member.team_id))) .where(({ member }) => eq(member.role, `admin`)) .select(({ team, member }) => ({ team_id: team.id, @@ -592,10 +569,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .innerJoin( - { member: teamMembersCollection }, - ({ team, member }) => eq(team.id, member.team_id), - ) + .join(innerJoin({ member: teamMembersCollection }, + ({ team, member }) => eq(team.id, member.team_id))) .where(({ member }) => eq(member.user_id, 100)) .select(({ team, member }) => ({ team_id: team.id, @@ -733,16 +708,10 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ company: companiesCollection }) - .join( - { project: projectsCollection }, - ({ company, project }) => eq(project.company_id, company.id), - joinType, - ) - .join( - { task: tasksCollection }, - ({ task, project }) => eq(task.project_id, project.id), - joinType, - ) + .join(joinOf(joinType, { project: projectsCollection }, + ({ company, project }) => eq(project.company_id, company.id))) + .join(joinOf(joinType, { task: tasksCollection }, + ({ task, project }) => eq(task.project_id, project.id))) .select(({ company, project, task }) => ({ company_name: company.name, project_name: project.name, @@ -822,11 +791,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `inner`, - ) + .join(innerJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -897,11 +863,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: emptyUsers }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `inner`, - ) + .join(innerJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -936,11 +899,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `left`, - ) + .join(leftJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_id: user.id, user_name: user.name, @@ -972,15 +932,12 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, + .join(innerJoin({ dept: departmentsCollection }, ({ user, dept }) => eq( concat(`dept`, user.department_id), concat(`dept`, dept.id), - ), - `inner`, - ) + ))) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -1031,9 +988,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ left: leftCollection }) - .innerJoin({ right: rightCollection }, ({ left, right }) => - eq(left.joinedAt, right.joinedAt), - ) + .join(innerJoin({ right: rightCollection }, ({ left, right }) => + eq(left.joinedAt, right.joinedAt))) .select(({ left, right }) => ({ leftId: left.id, rightId: right.id, @@ -1083,9 +1039,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ left: leftCollection }) - .innerJoin({ right: rightCollection }, ({ left, right }) => - eq(left.joinedAt, right.joinedAt), - ) + .join(innerJoin({ right: rightCollection }, ({ left, right }) => + eq(left.joinedAt, right.joinedAt))) .select(({ left, right }) => ({ leftId: left.id, rightId: right.id, @@ -1189,12 +1144,10 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ player: playersCollection }) - .innerJoin({ client: clientsCollection }, ({ client, player }) => - eq(client.player, player.name), - ) - .innerJoin({ balance: balancesCollection }, ({ balance, client }) => - eq(balance.client, client.name), - ) + .join(innerJoin({ client: clientsCollection }, ({ client, player }) => + eq(client.player, player.name))) + .join(innerJoin({ balance: balancesCollection }, ({ balance, client }) => + eq(balance.client, client.name))) .select(({ player, client, balance }) => ({ player_name: player.name, client_name: client.name, @@ -1337,11 +1290,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ users: selfJoinUsersCollection }) - .join( - { parentUsers: selfJoinUsersCollection }, - ({ users, parentUsers }) => eq(users.parentId, parentUsers.id), - `inner`, - ) + .join(innerJoin({ parentUsers: selfJoinUsersCollection }, + ({ users, parentUsers }) => eq(users.parentId, parentUsers.id))) .select(({ users, parentUsers }) => ({ user_id: users.id, user_name: users.name, @@ -1409,11 +1359,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ users: bidirectionalUsersCollection }) - .join( - { parentUsers: bidirectionalUsersCollection }, - ({ users, parentUsers }) => eq(users.parentId, parentUsers.id), - `inner`, - ) + .join(innerJoin({ parentUsers: bidirectionalUsersCollection }, + ({ users, parentUsers }) => eq(users.parentId, parentUsers.id))) .select(({ users, parentUsers }) => ({ user_name: users.name, parent_name: parentUsers.name, @@ -1431,11 +1378,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ users: bidirectionalUsersCollection }) - .join( - { parentUsers: bidirectionalUsersCollection }, - ({ users, parentUsers }) => eq(parentUsers.id, users.parentId), - `inner`, - ) + .join(innerJoin({ parentUsers: bidirectionalUsersCollection }, + ({ users, parentUsers }) => eq(parentUsers.id, users.parentId))) .select(({ users, parentUsers }) => ({ user_name: users.name, parent_name: parentUsers.name, @@ -1475,11 +1419,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { createLiveQueryCollection({ startSync: true, query: (q) => - q.from({ user: usersCollection }).join( - { dept: departmentsCollection }, - ({ user }) => eq(user.id, user.department_id), // Both refer to 'user' table - `inner`, - ), + q.from({ user: usersCollection }).join(innerJoin({ dept: departmentsCollection }, + ({ user }) => eq(user.id, user.department_id))), }) }).toThrow( `Invalid join condition: both expressions refer to the same source "user"`, @@ -1501,11 +1442,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { createLiveQueryCollection({ startSync: true, query: (q) => - q.from({ user: usersCollection }).join( - { dept: departmentsCollection }, - () => eq(1, 2), // Constants, no table references - `inner`, - ), + q.from({ user: usersCollection }).join(innerJoin({ dept: departmentsCollection }, + () => eq(1, 2))), }) }).toThrow( `Invalid join condition: expressions must reference source aliases`, @@ -1531,16 +1469,10 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(dept.id, user.department_id), - `inner`, - ) - .join( - { dept2: departmentsCollection2 }, - ({ user, dept }) => eq(dept.id, user.department_id), - `inner`, - ), + .join(innerJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(dept.id, user.department_id))) + .join(innerJoin({ dept2: departmentsCollection2 }, + ({ user, dept }) => eq(dept.id, user.department_id))), }) }).toThrow( `Invalid join condition: right expression does not refer to the joined source "dept2"`, @@ -1562,11 +1494,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { createLiveQueryCollection({ startSync: true, query: (q) => - q.from({ user: usersCollection }).join( - { dept: departmentsCollection }, - ({ user }) => eq(user.id, user.department_id), // Both refer to 'user' table - `inner`, - ), + q.from({ user: usersCollection }).join(innerJoin({ dept: departmentsCollection }, + ({ user }) => eq(user.id, user.department_id))), }) }).toThrow( `Invalid join condition: both expressions refer to the same source "user"`, @@ -1588,10 +1517,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: usersCollection }) - .leftJoin( - { special: specialUsersCollection }, - ({ user, special }) => eq(user.id, special.id), - ) + .join(leftJoin({ special: specialUsersCollection }, + ({ user, special }) => eq(user.id, special.id))) .where(({ special }) => isUndefined(special)), }) @@ -1658,11 +1585,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { .where(({ event }) => eq(event.id, `3770a4a6-3260-4566-9f79-f50864ebdd46`), ) - .join( - { parent: eventCollection }, - ({ event, parent }) => eq(parent.id, event.parent_id), - `left`, - ) + .join(leftJoin({ parent: eventCollection }, + ({ event, parent }) => eq(parent.id, event.parent_id))) .select(({ event, parent }) => ({ id: event.id, parent_id: event.parent_id, @@ -1723,11 +1647,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ employee: peopleCollection }) .where(({ employee }) => gt(employee.age, 30)) - .join( - { manager: peopleCollection }, - ({ employee, manager }) => eq(employee.manager_id, manager.id), - `left`, - ) + .join(leftJoin({ manager: peopleCollection }, + ({ employee, manager }) => eq(employee.manager_id, manager.id))) .where(({ manager }) => or(isNull(manager.id), gt(manager.age, 35))) .select(({ employee, manager }) => ({ employeeId: employee.id, @@ -1812,24 +1733,18 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ main: collection1 }) - .join( - { + .join(leftJoin({ join1: q .from({ join1: collection2 }) .where(({ join1 }) => not(gt(join1.other, 20))), }, - ({ main, join1 }) => eq(main.value, join1.value), - `left`, - ) - .join( - { + ({ main, join1 }) => eq(main.value, join1.value))) + .join(leftJoin({ join2: q .from({ join2: collection2 }) .where(({ join2 }) => not(lt(join2.other, 20))), }, - ({ main, join2 }) => eq(main.value, join2.value), - `left`, - ), + ({ main, join2 }) => eq(main.value, join2.value))), }) const multipleResults = multipleJoinQuery.toArray @@ -1899,7 +1814,7 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ l: leftCollection }) - .leftJoin({ r: rightCollection }, ({ l, r }) => eq(l.rightId, r.id)) + .join(leftJoin({ r: rightCollection }, ({ l, r }) => eq(l.rightId, r.id))) .where(({ r }) => isUndefined(r.payload)) .select(({ l, r }) => ({ leftId: l.id, right: r })), }) @@ -1993,16 +1908,10 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ player: playersCollection }) - .join( - { client: clientsCollection }, - ({ client, player }) => eq(client.player, player.name), - `left`, - ) - .join( - { balance: balancesCollection }, - ({ balance, client }) => eq(balance.client, client.name), - `left`, - ) + .join(leftJoin({ client: clientsCollection }, + ({ client, player }) => eq(client.player, player.name))) + .join(leftJoin({ balance: balancesCollection }, + ({ balance, client }) => eq(balance.client, client.name))) .select(({ player, client, balance }) => ({ player_name: player.name, client_name: client.name, @@ -2092,9 +2001,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ task: taskCollection }) .where(({ task }) => inArray(task.id, [1])) - .innerJoin({ project: projectCollection }, ({ task, project }) => - eq(task.project_id, project.id), - ) + .join(innerJoin({ project: projectCollection }, ({ task, project }) => + eq(task.project_id, project.id))) .select(({ task, project }) => ({ task, project, diff --git a/packages/db/tests/query/live-query-collection.test.ts b/packages/db/tests/query/live-query-collection.test.ts index 99f211ceb6..3d6ea6484a 100644 --- a/packages/db/tests/query/live-query-collection.test.ts +++ b/packages/db/tests/query/live-query-collection.test.ts @@ -1,14 +1,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Temporal } from 'temporal-polyfill' -import { createCollection } from '../../src/collection/index.js' -import { - and, - coalesce, +import { and, coalesce, createLiveQueryCollection, eq, ilike, - liveQueryCollectionOptions, -} from '../../src/query/index.js' + innerJoin, + leftJoin, + liveQueryCollectionOptions } from '../../src/query/index.js' +import { createCollection } from '../../src/collection/index.js' import { Query } from '../../src/query/builder/index.js' import { flushPromises, @@ -914,12 +913,9 @@ describe(`createLiveQueryCollection`, () => { query: (q: any) => q .from({ thread: sourceLQ }) - .join( - { label: labelsByEmail }, + .join(innerJoin({ label: labelsByEmail }, ({ thread, label }: any) => - eq(thread.last_email_id, label.email_id), - `inner`, - ) + eq(thread.last_email_id, label.email_id))) .orderBy(({ thread }: any) => thread.last_sent_at, { direction: `desc`, }), @@ -1107,14 +1103,10 @@ describe(`createLiveQueryCollection`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ player: playerCollection }) - .leftJoin( - { challenge1: challenge1Collection }, - ({ player, challenge1 }) => eq(player.id, challenge1.id), - ) - .leftJoin( - { challenge2: challenge2Collection }, - ({ player, challenge2 }) => eq(player.id, challenge2.id), - ), + .join(leftJoin({ challenge1: challenge1Collection }, + ({ player, challenge1 }) => eq(player.id, challenge1.id))) + .join(leftJoin({ challenge2: challenge2Collection }, + ({ player, challenge2 }) => eq(player.id, challenge2.id))), ) // Start the query, but don't wait it, we are doing to write the data to the @@ -1971,9 +1963,8 @@ describe(`createLiveQueryCollection`, () => { const live = createLiveQueryCollection((q) => q .from({ user: users }) - .leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ) + .join(leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId))) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2017,9 +2008,8 @@ describe(`createLiveQueryCollection`, () => { const live = createLiveQueryCollection((q) => q .from({ user: users }) - .leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ) + .join(leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId))) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2116,9 +2106,8 @@ describe(`createLiveQueryCollection`, () => { const live = createLiveQueryCollection((q) => q .from({ parent: parents }) - .leftJoin({ child: children }, ({ parent, child }) => - eq(parent.id, child.parentId), - ) + .join(leftJoin({ child: children }, ({ parent, child }) => + eq(parent.id, child.parentId))) .orderBy(({ parent }) => parent.rank, `asc`) .limit(1) .select(({ parent, child }) => ({ @@ -3976,11 +3965,8 @@ describe(`createLiveQueryCollection`, () => { const userPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join( - { post: posts }, - ({ user, post }) => eq(user.id, post.authorId), - `inner`, - ) + .join(innerJoin({ post: posts }, + ({ user, post }) => eq(user.id, post.authorId))) .where(({ user, post }) => and(eq(user.active, true), eq(post.published, true)), ) @@ -4486,7 +4472,7 @@ describe(`createLiveQueryCollection`, () => { query: (q) => q .from({ base }) - .join({ related }, ({ base: b, related: r }) => eq(b.id, r.id)) + .join(leftJoin({ related }, ({ base: b, related: r }) => eq(b.id, r.id))) .select(({ base: b, related: r }) => ({ id: b.id, name: b.name, @@ -4527,7 +4513,7 @@ describe(`createLiveQueryCollection`, () => { query: (q) => q .from({ comments }) - .join({ users }, ({ comments: c, users: u }) => eq(c.userId, u.id)) + .join(leftJoin({ users }, ({ comments: c, users: u }) => eq(c.userId, u.id))) .select(({ comments: c, users: u }) => ({ id: c.id, userId: coalesce(u.id, c.userId), @@ -4920,9 +4906,8 @@ describe(`createLiveQueryCollection`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ task: taskCollection }) - .leftJoin({ project: projectCollection }, ({ task, project }) => - eq(task.project_id, project.id), - ), + .join(leftJoin({ project: projectCollection }, ({ task, project }) => + eq(task.project_id, project.id))), ) const preloadPromise = liveQuery.preload() @@ -5036,9 +5021,8 @@ describe(`createLiveQueryCollection`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ task: taskCollection }) - .leftJoin({ project: projectCollection }, ({ task, project }) => - eq(task.project_id, project.id), - ), + .join(leftJoin({ project: projectCollection }, ({ task, project }) => + eq(task.project_id, project.id))), ) const preloadPromise = liveQuery.preload() @@ -5121,9 +5105,8 @@ describe(`createLiveQueryCollection`, () => { query: (q) => q .from({ team: teamsCollection }) - .join({ user: usersCollection }, ({ team, user }) => - eq(team.lead_id, user.id), - ) + .join(leftJoin({ user: usersCollection }, ({ team, user }) => + eq(team.lead_id, user.id))) .select(({ team, user }) => ({ teamName: team.name, leadName: user.name, diff --git a/packages/db/tests/query/load-subset-join-dedupe.test.ts b/packages/db/tests/query/load-subset-join-dedupe.test.ts index 54427f5920..1e1a55e4fb 100644 --- a/packages/db/tests/query/load-subset-join-dedupe.test.ts +++ b/packages/db/tests/query/load-subset-join-dedupe.test.ts @@ -1,8 +1,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' +import { createLiveQueryCollection, eq, leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BasicIndex } from '../../src/indexes/basic-index.js' import { extractSimpleComparisons } from '../../src/query/expression-helpers.js' -import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { expectAssertionFailure } from '../expected-failure.js' import { TraceAssertionError } from '../trace-runner.js' import { flushPromises } from '../utils.js' @@ -92,9 +92,8 @@ function createJoinedQuery( const live = createLiveQueryCollection((query) => query .from({ parent: parentCollection }) - .join({ child: childCollection }, ({ parent, child }) => - eq(child.parentId, parent.id), - ), + .join(leftJoin({ child: childCollection }, ({ parent, child }) => + eq(child.parentId, parent.id))), ) cleanups.push(() => live.cleanup()) return live diff --git a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts index 32ec955ad6..0affb65674 100644 --- a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts +++ b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts @@ -1,14 +1,13 @@ import fc from 'fast-check' import { describe, expect, it, vi } from 'vitest' -import { createCollection } from '../../src/collection/index.js' -import { createDeferred } from '../../src/deferred.js' -import { - createLiveQueryCollection, - eq, +import { createLiveQueryCollection, eq, + innerJoin, isUndefined, + leftJoin, not, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' +import { createCollection } from '../../src/collection/index.js' +import { createDeferred } from '../../src/deferred.js' import { BasicIndex } from '../../src/indexes/basic-index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { evaluateReferenceExpression } from '../reference-expression-oracle.js' @@ -925,11 +924,9 @@ describe(`loadSubset replay refinement`, () => { const live = createLiveQueryCollection((q) => q .from({ primary: primary.collection }) - .innerJoin( - { secondary: secondary.collection }, + .join(innerJoin({ secondary: secondary.collection }, ({ primary: left, secondary: right }) => - eq(left.joinKey, right.joinKey), - ) + eq(left.joinKey, right.joinKey))) .orderBy(({ primary: row }) => row.version) .limit(1) .select(({ primary: left, secondary: right }) => ({ @@ -1588,14 +1585,12 @@ async function checkOrderedReplay( const from = q.from({ row: source }) const filtered = scenario.joined === `inner` - ? from.innerJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId), - ) + ? from.join(innerJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId))) : scenario.joined ? from - .leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId), - ) + .join(leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId))) .where(({ marker }) => scenario.joined === `none` ? isUndefined(marker.rowId) diff --git a/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts b/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts index 9fa17aec30..f945eeb9c2 100644 --- a/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts +++ b/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts @@ -1,7 +1,7 @@ import { expect, it } from 'vitest' +import { BTreeIndex, innerJoin } from '../../src/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' -import { BTreeIndex } from '../../src/index.js' import { extractSimpleComparisons } from '../../src/query/expression-helpers.js' import { createLiveQueryCollection, @@ -406,9 +406,8 @@ it.each([`resolve`, `reject`, `cleanup`] as const)( query: (q) => q .from({ left }) - .innerJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.group, rightRow.group), - ) + .join(innerJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.group, rightRow.group))) .select(({ left: leftRow, right: rightRow }) => ({ leftId: leftRow.id, rightId: rightRow.id, diff --git a/packages/db/tests/query/load-subset-subquery.test.ts b/packages/db/tests/query/load-subset-subquery.test.ts index ead6b921d3..4189eae4ce 100644 --- a/packages/db/tests/query/load-subset-subquery.test.ts +++ b/packages/db/tests/query/load-subset-subquery.test.ts @@ -1,10 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createCollection } from '../../src/collection/index.js' -import { - and, +import { and, coalesce, createLiveQueryCollection, eq, + fullJoin, gt, gte, inArray, @@ -12,8 +11,8 @@ import { lt, lte, not, - or, -} from '../../src/query/index.js' + or } from '../../src/query/index.js' +import { createCollection } from '../../src/collection/index.js' import { PropRef, Value } from '../../src/query/ir.js' import type { Collection } from '../../src/collection/index.js' import type { @@ -287,9 +286,8 @@ describe(`loadSubset with subqueries`, () => { // Use subquery in main query return q .from({ charge: chargesCollection }) - .fullJoin({ prepaidOrder: prepaidOrderQ }, ({ charge, prepaidOrder }) => - eq(charge.address_id, prepaidOrder.address_id), - ) + .join(fullJoin({ prepaidOrder: prepaidOrderQ }, ({ charge, prepaidOrder }) => + eq(charge.address_id, prepaidOrder.address_id))) }) cleanups.push(ordersCollection, subqueryQuery) @@ -361,9 +359,8 @@ describe(`loadSubset with subqueries`, () => { // Use subquery in main query return q .from({ charge: chargesCollection }) - .fullJoin({ prepaidOrder: prepaidOrderQ }, ({ charge, prepaidOrder }) => - eq(charge.address_id, prepaidOrder.address_id), - ) + .join(fullJoin({ prepaidOrder: prepaidOrderQ }, ({ charge, prepaidOrder }) => + eq(charge.address_id, prepaidOrder.address_id))) }) cleanups.push(ordersCollection, subqueryQuery) @@ -407,9 +404,8 @@ describe(`loadSubset with subqueries`, () => { return q .from({ charge: chargesCollection }) - .fullJoin({ order: orderedOrders }, ({ charge, order }) => - eq(charge.address_id, order.address_id), - ) + .join(fullJoin({ order: orderedOrders }, ({ charge, order }) => + eq(charge.address_id, order.address_id))) }) cleanups.push(ordersCollection, query) diff --git a/packages/db/tests/query/optimizer-semantics-oracle.test.ts b/packages/db/tests/query/optimizer-semantics-oracle.test.ts index 7d53c68b7b..391b13ebfb 100644 --- a/packages/db/tests/query/optimizer-semantics-oracle.test.ts +++ b/packages/db/tests/query/optimizer-semantics-oracle.test.ts @@ -1,3 +1,7 @@ +import { describe, expect, test } from 'vitest' +import { applyOptimizations } from '../../src/query/optimizer-joins.js' +import { processJoins } from '../../src/query/compiler/joins.js' +import { createLiveQueryCollection, innerJoin, leftJoin, rightJoin } from '../../src/query/index.js' /** * An outer predicate on an aggregate subquery's result must run after the * aggregate. Moving it to source rows can change or remove the result. @@ -21,9 +25,7 @@ * supplies a second formulation for matching and nonmatching controls. * The nested matching cases also reject an all-empty QueryRef implementation. */ -import { describe, expect, test } from 'vitest' import { createCollection } from '../../src/collection/index.js' -import { createLiveQueryCollection } from '../../src/query/index.js' import { optimizeQuery } from '../../src/query/optimizer.js' import { add, @@ -109,7 +111,7 @@ describe('optimizer aggregate semantics', () => { })) const joined = q .from({ s: summary }) - .innerJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)) + .join(innerJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target))) if (scenario.filter === `accept`) { return joined .where(({ s }) => eq(s.k, 2)) @@ -140,7 +142,7 @@ describe('optimizer aggregate semantics', () => { query: (q) => q .from({ s: aggregate }) - .innerJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)) + .join(innerJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target))) .select(({ s }) => ({ total: s.total })), }) expect(materialized.toArray.map(stripVirtualProps)).toEqual( @@ -200,7 +202,7 @@ describe('optimizer aggregate semantics', () => { query: (q) => q .from({ s: aggregate }) - .leftJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)) + .join(leftJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target))) .where(({ s }) => eq(s.k, 2)) .select(({ s }) => ({ total: s.total })), }) @@ -223,7 +225,7 @@ describe('optimizer aggregate semantics', () => { })) return q .from({ s: summary }) - .leftJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)) + .join(leftJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target))) .where(({ s }) => outerPredicate === `accept` ? eq(s.k, 2) : eq(s.k, 1), ) @@ -273,7 +275,7 @@ describe('optimizer aggregate semantics', () => { : base.select(({ b }) => ({ k: 1, tag: `x`, total: sum(b.v) })) return q .from({ s: summary }) - .leftJoin({ a: anchor }, ({ s, a }) => eq(s.k, a.target)) + .join(leftJoin({ a: anchor }, ({ s, a }) => eq(s.k, a.target))) .where(({ s }) => eq(s.tag, `x`)) .select(({ s }) => ({ k: s.k, total: s.total })) }, @@ -315,7 +317,7 @@ describe('optimizer aggregate semantics', () => { .select(({ b }) => ({ k: b.k, total: add(sum(b.v), 0) })) return q .from({ s: summary }) - .leftJoin({ a: anchor }, ({ s, a }) => eq(s.k, a.target)) + .join(leftJoin({ a: anchor }, ({ s, a }) => eq(s.k, a.target))) .where(({ s }) => eq(s.k, 1)) .select(({ s }) => ({ k: s.k, total: s.total })) }, @@ -498,7 +500,7 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: join.type, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -562,7 +564,7 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -570,7 +572,7 @@ describe(`optimizer residual convergence`, () => { new PropRef([`member`, `teamId`]), ]), }, - { + { compile: processJoins, optimize: applyOptimizations, type: `inner`, from: new CollectionRef(collection, `tag`), on: new Func('eq', [ @@ -617,7 +619,7 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: joinType, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -649,7 +651,7 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -682,7 +684,7 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -711,7 +713,7 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -749,7 +751,7 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -757,7 +759,7 @@ describe(`optimizer residual convergence`, () => { new PropRef([`member`, `teamId`]), ]), }, - { + { compile: processJoins, optimize: applyOptimizations, type: laterJoin, from: new CollectionRef(collection, `tag`), on: new Func('eq', [ @@ -818,7 +820,7 @@ describe(`optimizer residual convergence`, () => { `team`, ), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -857,7 +859,7 @@ describe(`optimizer residual convergence`, () => { `team`, ), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -916,9 +918,8 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .leftJoin({ member: memberCollection }, ({ team, member }) => - eq(team.id, member.teamId), - ) + .join(leftJoin({ member: memberCollection }, ({ team, member }) => + eq(team.id, member.teamId))) .where(({ team }) => eq(team.active, true)) .where(() => new Value(false)), }) @@ -976,9 +977,8 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .leftJoin({ member: memberCollection }, ({ team, member }) => - eq(team.id, member.teamId), - ) + .join(leftJoin({ member: memberCollection }, ({ team, member }) => + eq(team.id, member.teamId))) .where(({ team }) => eq(team.active, true)) .where(({ member }) => or(isUndefined(member.id), eq(member.userId, 100)), @@ -993,9 +993,8 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .rightJoin({ member: memberCollection }, ({ team, member }) => - eq(team.id, member.teamId), - ) + .join(rightJoin({ member: memberCollection }, ({ team, member }) => + eq(team.id, member.teamId))) .where(({ member }) => eq(member.userId, 100)) .where(({ team }) => or(isUndefined(team.id), eq(team.active, true)), @@ -1071,12 +1070,10 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .leftJoin({ member: memberCollection }, ({ team, member }) => - eq(team.id, member.teamId), - ) - .innerJoin({ tag: tagCollection }, ({ team, tag }) => - eq(team.id, tag.teamId), - ) + .join(leftJoin({ member: memberCollection }, ({ team, member }) => + eq(team.id, member.teamId))) + .join(innerJoin({ tag: tagCollection }, ({ team, tag }) => + eq(team.id, tag.teamId))) .where(({ member }) => or(isUndefined(member.id), eq(member.userId, 100)), ) diff --git a/packages/db/tests/query/optimizer.test.ts b/packages/db/tests/query/optimizer.test.ts index 7545fd5e62..02badefe29 100644 --- a/packages/db/tests/query/optimizer.test.ts +++ b/packages/db/tests/query/optimizer.test.ts @@ -1,4 +1,6 @@ import { describe, expect, test } from 'vitest' +import { applyOptimizations } from '../../src/query/optimizer-joins.js' +import { processJoins } from '../../src/query/compiler/joins.js' import { optimizeQuery } from '../../src/query/optimizer.js' import { Aggregate, @@ -133,7 +135,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -164,7 +166,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -206,7 +208,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new QueryRef(distinctSource, `p`), type: `inner`, on: createEq(createPropRef(`u`, `id`), createPropRef(`p`, `id`)), @@ -227,7 +229,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -275,7 +277,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -314,7 +316,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -351,7 +353,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -388,7 +390,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -431,7 +433,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -491,7 +493,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -525,7 +527,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -566,7 +568,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -603,7 +605,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(existingSubquery, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -648,7 +650,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(deeplyNestedQuery, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -676,7 +678,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -701,7 +703,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -756,7 +758,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -779,7 +781,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -814,7 +816,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -857,7 +859,7 @@ describe(`Query Optimizer`, () => { `u`, ), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -905,7 +907,7 @@ describe(`Query Optimizer`, () => { `u`, ), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -961,7 +963,7 @@ describe(`Query Optimizer`, () => { source.bindingId, ), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -998,7 +1000,7 @@ describe(`Query Optimizer`, () => { `u`, ), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new QueryRef( { from: new CollectionRef(mockCollection, `p`), @@ -1078,7 +1080,7 @@ describe(`Query Optimizer`, () => { `u`, ), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1111,7 +1113,7 @@ describe(`Query Optimizer`, () => { const simpleQuery: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1147,7 +1149,7 @@ describe(`Query Optimizer`, () => { // Add JOIN and WHERE to make it optimizable deepQuery.join = [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq(createPropRef(`u`, `id`), createPropRef(`p`, `user_id`)), @@ -1173,7 +1175,7 @@ describe(`Query Optimizer`, () => { `u`, ), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1229,7 +1231,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(sharedSubquery, `main_users`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new QueryRef(sharedSubquery, `other_users`), // Same subquery object! type: `inner`, on: createEq( @@ -1289,7 +1291,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1332,7 +1334,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithLimitedOrder, `top_users`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1369,7 +1371,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithSelect, `filtered_users`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1417,7 +1419,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithHaving, `dept_stats`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1452,7 +1454,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithFnSelect, `computed_users`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1500,7 +1502,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithOrderOnly, `sorted_users`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1552,7 +1554,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(safeSubquery, `users`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new QueryRef(unsafeSubquery, `dept_stats`), type: `inner`, on: createEq( @@ -1599,7 +1601,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1659,7 +1661,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1749,7 +1751,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(teamsCollection, `team`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `left`, from: new CollectionRef(teamMembersCollection, `teamMember`), on: createEq( @@ -1794,7 +1796,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(usersCollection, `user`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `right`, from: new CollectionRef(profilesCollection, `profile`), on: createEq( @@ -1844,7 +1846,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(ordersCollection, `order`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `full`, from: new CollectionRef(paymentsCollection, `payment`), on: createEq( @@ -1889,7 +1891,7 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(usersCollection, `user`), join: [ - { + { compile: processJoins, optimize: applyOptimizations, type: `inner`, from: new CollectionRef(departmentsCollection, `dept`), on: createEq( diff --git a/packages/db/tests/query/optional-fields-negative.test-d.ts b/packages/db/tests/query/optional-fields-negative.test-d.ts index b27e0743ad..a77313b820 100644 --- a/packages/db/tests/query/optional-fields-negative.test-d.ts +++ b/packages/db/tests/query/optional-fields-negative.test-d.ts @@ -1,5 +1,5 @@ import { describe, expectTypeOf, test } from 'vitest' -import { createLiveQueryCollection, eq, gt } from '../../src/query/index.js' +import { createLiveQueryCollection, eq, gt, innerJoin, leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -84,14 +84,11 @@ describe(`Optional Fields - Type Safety Tests`, () => { const query = createLiveQueryCollection({ query: (q) => - q.from({ user: usersCollection }).join( - { dept: departmentsCollection }, + q.from({ user: usersCollection }).join(innerJoin({ dept: departmentsCollection }, ({ user, dept }) => { // This should work correctly - department_id is optional but can be used in join return eq(user.department_id, dept.id) - }, - `inner`, - ), + })), }) expectTypeOf(query.toArray).toMatchTypeOf< @@ -162,11 +159,8 @@ describe(`Optional Fields - Type Safety Tests`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `left`, - ) + .join(leftJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, diff --git a/packages/db/tests/query/optional-fields-runtime.test.ts b/packages/db/tests/query/optional-fields-runtime.test.ts index 3778eb7d8c..4a42666773 100644 --- a/packages/db/tests/query/optional-fields-runtime.test.ts +++ b/packages/db/tests/query/optional-fields-runtime.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { createLiveQueryCollection, eq } from '../../src/query/index.js' +import { createLiveQueryCollection, eq, leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -159,11 +159,8 @@ describe(`Optional Fields - Runtime Tests`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `left`, - ) + .join(leftJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, diff --git a/packages/db/tests/query/order-by.test.ts b/packages/db/tests/query/order-by.test.ts index ba55fbc209..cdee5fc597 100644 --- a/packages/db/tests/query/order-by.test.ts +++ b/packages/db/tests/query/order-by.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest' +import { leftJoin } from '@tanstack/db' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' import { createLiveQueryCollection } from '../../src/query/live-query-collection.js' @@ -831,11 +832,9 @@ function createOrderByTests(autoIndex: `off` | `eager`): void { const collection = createLiveQueryCollection((q) => q .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .orderBy(({ departments }) => departments.name, `asc`) .orderBy(({ employees }) => employees.salary, `desc`) .select(({ employees, departments }) => ({ @@ -1900,11 +1899,9 @@ function createOrderByTests(autoIndex: `off` | `eager`): void { const collection = createLiveQueryCollection((q) => q .from({ employees: employeesCollection }) - .join( - { departments: departmentsCollection }, + .join(leftJoin({ departments: departmentsCollection }, ({ employees, departments }) => - eq(employees.department_id, departments.id), - ) + eq(employees.department_id, departments.id))) .orderBy(({ departments }) => departments.name, `asc`) .limit(5) .select(({ employees, departments }) => ({ @@ -1940,9 +1937,8 @@ function createOrderByTests(autoIndex: `off` | `eager`): void { const collection = createLiveQueryCollection((q) => q .from({ employee: employeesCollection }) - .join({ manager: employeesCollection }, ({ employee, manager }) => - eq(employee.id, manager.id), - ) + .join(leftJoin({ manager: employeesCollection }, ({ employee, manager }) => + eq(employee.id, manager.id))) .orderBy(({ manager }) => manager.name, `asc`) .limit(3) .select(({ employee, manager }) => ({ diff --git a/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts b/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts index 281ffd4788..8bba0da9a7 100644 --- a/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts +++ b/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts @@ -1,16 +1,15 @@ import { isDeepStrictEqual } from 'node:util' import { describe, expect, it } from 'vitest' import { fc, test as fcTest } from '@fast-check/vitest' +import { createLiveQueryCollection, eq, + innerJoin, + isUndefined, + leftJoin, + not, + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' -import { - createLiveQueryCollection, - eq, - isUndefined, - not, - toArray, -} from '../../src/query/index.js' import { evaluateReferenceExpression } from '../reference-expression-oracle.js' import { captureMicrotaskErrors, flushPromises } from '../utils.js' import { @@ -1115,9 +1114,8 @@ describe(`synchronous initial settlement refinement`, () => { query: (q) => q .from({ row: rows }) - .leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId), - ) + .join(leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId))) .where(({ marker }) => not(isUndefined(marker.rowId))) .fn.where(({ row }) => row.rank > 0) .orderBy(({ row }) => row.rank) @@ -1219,9 +1217,8 @@ describe(`synchronous initial settlement refinement`, () => { query: (q) => q .from({ left }) - .leftJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.group, rightRow.group), - ) + .join(leftJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.group, rightRow.group))) .orderBy(({ left: leftRow }) => leftRow.rank) .limit(2) .select(({ left: leftRow, right: rightRow }) => ({ @@ -2020,9 +2017,8 @@ async function observeLaterFallback( query: (q) => q .from({ root: rootCollection }) - .leftJoin({ marker: markerCollection }, ({ root, marker }) => - eq(root.id, marker.rootId), - ) + .join(leftJoin({ marker: markerCollection }, ({ root, marker }) => + eq(root.id, marker.rootId))) .where(({ marker }) => not(isUndefined(marker.id))) .orderBy(({ root }) => root.at, { direction: `asc`, @@ -3072,7 +3068,7 @@ function createFullSourceQuery( switch (feature) { case `inner-join`: return base - .innerJoin({ tag: tags }, ({ row, tag }) => eq(row.tag, tag.tag)) + .join(innerJoin({ tag: tags }, ({ row, tag }) => eq(row.tag, tag.tag))) .orderBy(({ row }) => row.rank) .limit(2) .select(({ row }) => ({ id: row.id })) diff --git a/packages/db/tests/query/ordered-work-oracle.property.test.ts b/packages/db/tests/query/ordered-work-oracle.property.test.ts index 8ef2cd6d1d..51f8448ece 100644 --- a/packages/db/tests/query/ordered-work-oracle.property.test.ts +++ b/packages/db/tests/query/ordered-work-oracle.property.test.ts @@ -1,5 +1,6 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, it, vi } from 'vitest' +import { fullJoin as fullJoinOperator, innerJoin, leftJoin, toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { localOnlyCollectionOptions } from '../../src/local-only.js' import { createDeferred } from '../../src/deferred.js' @@ -11,7 +12,6 @@ import { getLoadSubsetDemandKey } from '../../src/query/ir-stable-identity.js' import { DeduplicatedLoadSubset } from '../../src/query/subset-dedupe.js' import { Func, PropRef, Value } from '../../src/query/ir.js' import { createLiveQueryCollection } from '../../src/query/live-query-collection.js' -import { toArray } from '../../src/query/index.js' import { eq, gte, isUndefined, not } from '../../src/query/builder/functions.js' import { oracleRandomParameters, @@ -467,9 +467,8 @@ async function observeConsumer( const query = (q: InitialQueryBuilder) => { const joined = q .from({ row: source }) - .leftJoin({ marker: markerSource }, ({ row, marker }) => - eq(row.id, joinFilter === `to-one` ? marker.id : marker.rowId), - ) + .join(leftJoin({ marker: markerSource }, ({ row, marker }) => + eq(row.id, joinFilter === `to-one` ? marker.id : marker.rowId))) .where(({ row, marker }) => { switch (joinFilter) { case `joined-only`: @@ -1943,9 +1942,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .leftJoin({ marker }, ({ row, marker: child }) => - eq(row.id, child.rowId), - ) + .join(leftJoin({ marker }, ({ row, marker: child }) => + eq(row.id, child.rowId))) .orderBy(({ row }) => row.rank) .limit(1) .select(({ row }) => ({ @@ -2039,9 +2037,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .leftJoin({ marker: child }, ({ row, marker }) => - eq(row.id, marker.rowId), - ) + .join(leftJoin({ marker: child }, ({ row, marker }) => + eq(row.id, marker.rowId))) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) .limit(1) @@ -2108,9 +2105,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .leftJoin({ marker: joined }, ({ row, marker }) => - eq(row.id, marker.rowId), - ) + .join(leftJoin({ marker: joined }, ({ row, marker }) => + eq(row.id, marker.rowId))) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) .limit(0) @@ -2233,9 +2229,8 @@ describe(`ordered source work oracle`, () => { .from({ order: orders }) .where(({ order }) => gte(order.scheduledAt, `2024-01-12`)) .where(({ order }) => eq(order.status, `queued`)) - .innerJoin({ charge: charges }, ({ order, charge }) => - eq(order.addressId, charge.addressId), - ), + .join(innerJoin({ charge: charges }, ({ order, charge }) => + eq(order.addressId, charge.addressId))), ) try { @@ -2356,9 +2351,8 @@ describe(`ordered source work oracle`, () => { const query = (q: InitialQueryBuilder) => q .from({ row: source }) - .innerJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId), - ) + .join(innerJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId))) .orderBy(({ row }) => row.rank) .limit(0) const live = @@ -2604,11 +2598,9 @@ describe(`ordered source work oracle`, () => { const live = createLiveQueryCollection((q) => q .from({ primaryRow: primary }) - .innerJoin( - { secondaryRow: secondary }, + .join(innerJoin({ secondaryRow: secondary }, ({ primaryRow, secondaryRow }) => - eq(primaryRow.joinKey, secondaryRow.joinKey), - ) + eq(primaryRow.joinKey, secondaryRow.joinKey))) .orderBy(({ primaryRow }) => primaryRow.rank) .limit(2), ) @@ -2725,11 +2717,9 @@ describe(`ordered source work oracle`, () => { q .from({ primaryRow: primary }) .where(({ primaryRow }) => eq(primaryRow.id, id)) - .innerJoin( - { secondaryRow: secondary }, + .join(innerJoin({ secondaryRow: secondary }, ({ primaryRow, secondaryRow }) => - eq(primaryRow.joinKey, secondaryRow.joinKey), - ) + eq(primaryRow.joinKey, secondaryRow.joinKey))) .orderBy(({ primaryRow }) => primaryRow.rank) .limit(1), ) @@ -2881,9 +2871,8 @@ describe(`ordered source work oracle`, () => { const live = createLiveQueryCollection((q) => q .from({ row: primary }) - .leftJoin({ child: secondary }, ({ row, child }) => - eq(row.id, child.primaryId), - ) + .join(leftJoin({ child: secondary }, ({ row, child }) => + eq(row.id, child.primaryId))) .orderBy(({ row }) => row.rank) .limit(1) .select(({ row, child }) => ({ @@ -3134,9 +3123,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ root: rootCollection }) - .leftJoin({ child: childCollection }, ({ root, child }) => - eq(root.id, child.rootId), - ) + .join(leftJoin({ child: childCollection }, ({ root, child }) => + eq(root.id, child.rootId))) .where(({ child }) => not(isUndefined(child.rootId))) .orderBy(({ root }) => root.rank) .limit(1) @@ -3235,9 +3223,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ root: rootCollection }) - .leftJoin({ child: childCollection }, ({ root, child }) => - eq(root.id, child.rootId), - ) + .join(leftJoin({ child: childCollection }, ({ root, child }) => + eq(root.id, child.rootId))) .where(({ child }) => isUndefined(child.rootId)) .orderBy(({ root }) => root.rank) .limit(1) @@ -3409,9 +3396,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ root: rootCollection }) - .leftJoin({ child: childCollection }, ({ root, child }) => - eq(root.id, child.rootId), - ) + .join(leftJoin({ child: childCollection }, ({ root, child }) => + eq(root.id, child.rootId))) .where(({ child }) => isUndefined(child.rootId)) .orderBy(({ root }) => root.rank) .limit(1) @@ -4150,9 +4136,8 @@ describe(`ordered source work oracle`, () => { const query = (q: InitialQueryBuilder) => q .from({ row: root }) - .leftJoin({ marker: markerSource }, ({ row, marker }) => - eq(row.id, marker.rowId), - ) + .join(leftJoin({ marker: markerSource }, ({ row, marker }) => + eq(row.id, marker.rowId))) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) .limit(2) @@ -4263,9 +4248,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .leftJoin({ marker: joined }, ({ row, marker }) => - eq(row.id, marker.rowId), - ) + .join(leftJoin({ marker: joined }, ({ row, marker }) => + eq(row.id, marker.rowId))) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) .limit(1) @@ -5499,17 +5483,15 @@ async function runEagerWindow(testCase: EagerCase): Promise<{ ) => { const joinedRows = q .from({ row: source }) - .leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.link, marker.rowId), - ) + .join(leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.link, marker.rowId))) const ordered = ( fullJoin ? // Same aliases as the LEFT join; only the join kind differs. (q .from({ row: source }) - .fullJoin({ marker: markers }, ({ row, marker }) => - eq(row.link, marker.rowId), - ) as unknown as typeof joinedRows) + .join(fullJoinOperator({ marker: markers }, ({ row, marker }) => + eq(row.link, marker.rowId))) as unknown as typeof joinedRows) : joined ? admitsWhenMatched ? joinedRows.where(({ marker }) => not(isUndefined(marker.rowId))) @@ -6564,9 +6546,8 @@ async function checkOptimisticWindow( .where(({ row }) => eq(row.eligible, true)) const left = q .from({ row: source }) - .leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.link, marker.rowId), - ) + .join(leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.link, marker.rowId))) const filtered = shape === `source` ? direct diff --git a/packages/db/tests/query/pagination-oracle.property.test.ts b/packages/db/tests/query/pagination-oracle.property.test.ts index 7db4b3a22f..ea7e410a29 100644 --- a/packages/db/tests/query/pagination-oracle.property.test.ts +++ b/packages/db/tests/query/pagination-oracle.property.test.ts @@ -1,8 +1,8 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, it } from 'vitest' +import { BTreeIndex, innerJoin, leftJoin } from '../../src/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' -import { BTreeIndex } from '../../src/index.js' import { createLiveQueryCollection } from '../../src/query/live-query-collection.js' import { and, eq } from '../../src/query/builder/functions.js' import { PropRef } from '../../src/query/ir.js' @@ -3006,9 +3006,8 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ parent: parentSource }) - .innerJoin({ child: childSource }, ({ parent, child }) => - eq(parent.groupId, child.groupId), - ) + .join(innerJoin({ child: childSource }, ({ parent, child }) => + eq(parent.groupId, child.groupId))) .orderBy(({ parent }) => parent.rank, `asc`) .orderBy(({ parent }) => parent.id, `asc`) .limit(2) @@ -3055,9 +3054,8 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ parent: parentSource }) - .innerJoin({ child: childSource }, ({ parent, child }) => - eq(parent.groupId, child.groupId), - ) + .join(innerJoin({ child: childSource }, ({ parent, child }) => + eq(parent.groupId, child.groupId))) .orderBy(({ parent }) => parent.rank, `asc`) .orderBy(({ parent }) => parent.id, `asc`) .limit(2) @@ -3104,9 +3102,8 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ parent: parentSource }) - .leftJoin({ child: childSource }, ({ parent, child }) => - eq(parent.childId, child.id), - ) + .join(leftJoin({ child: childSource }, ({ parent, child }) => + eq(parent.childId, child.id))) .orderBy(({ parent }) => parent.sourceRank, `asc`) .orderBy(({ child }) => child.score, `asc`) .orderBy(({ parent }) => parent.id, `asc`) @@ -6545,9 +6542,8 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ scope }) - .innerJoin({ row: source }, ({ scope: outer, row }) => - eq(outer.groupId, row.groupId), - ) + .join(innerJoin({ row: source }, ({ scope: outer, row }) => + eq(outer.groupId, row.groupId))) .where(({ row }) => eq(row.keep, true)) .orderBy(({ row }) => row.label, { direction: `asc`, diff --git a/packages/db/tests/query/query-api-type-algebra.test-d.ts b/packages/db/tests/query/query-api-type-algebra.test-d.ts index 1eb7fe9212..0020d83181 100644 --- a/packages/db/tests/query/query-api-type-algebra.test-d.ts +++ b/packages/db/tests/query/query-api-type-algebra.test-d.ts @@ -1,3 +1,4 @@ +import { describe, expectTypeOf, test } from 'vitest' /** * Oracle owner: the query-builder compile-time suites. * @@ -12,8 +13,7 @@ * paired runtime test owns the unmatched-row value witness. Existing generic, * join, and nullable-leaf suites remain the broader compatibility owners. */ -import { describe, expectTypeOf, test } from 'vitest' -import { Query, createLiveQueryCollection, eq } from '../../src/query/index.js' +import { Query, createLiveQueryCollection, eq, fullJoin, leftJoin, rightJoin } from '../../src/query/index.js' import type { Collection } from '../../src/collection/index.js' import type { Context, @@ -65,9 +65,8 @@ describe(`query API type algebra`, () => { const query = createLiveQueryCollection((q) => q .from({ row: rows }) - .leftJoin({ department: departments }, ({ row, department }) => - eq(row.departmentId, department.id), - ) + .join(leftJoin({ department: departments }, ({ row, department }) => + eq(row.departmentId, department.id))) .select(({ department }) => ({ department, departmentName: department.name, @@ -124,15 +123,13 @@ describe(`query API type algebra`, () => { ) { const joinedBranchA = new Query() .from({ rowA: rowsA }) - .leftJoin({ otherA: othersA }, ({ rowA, otherA }) => - eq(rowA.id, otherA.id), - ) + .join(leftJoin({ otherA: othersA }, ({ rowA, otherA }) => + eq(rowA.id, otherA.id))) .select(({ otherA }) => ({ other: otherA })) const joinedBranchB = new Query() .from({ rowB: rowsB }) - .leftJoin({ otherB: othersB }, ({ rowB, otherB }) => - eq(rowB.id, otherB.id), - ) + .join(leftJoin({ otherB: othersB }, ({ rowB, otherB }) => + eq(rowB.id, otherB.id))) .select(({ otherB }) => ({ other: otherB })) const joinedUnion = new Query() .unionAll(joinedBranchA, joinedBranchB) @@ -187,9 +184,8 @@ describe(`query API type algebra`, () => { const query = createLiveQueryCollection((q) => q .from({ row: rows }) - .leftJoin({ department: departments }, ({ row, department }) => - eq(row.departmentId, department.id), - ) + .join(leftJoin({ department: departments }, ({ row, department }) => + eq(row.departmentId, department.id))) .select(({ department }) => ({ ...department })), ) @@ -217,11 +213,11 @@ describe(`query API type algebra`, () => { const joined = new Query() .from({ row: rows }) - .leftJoin({ item: a }, ({ row, item }) => { + .join(leftJoin({ item: a }, ({ row, item }) => { // @ts-expect-error Only the generic constraint is available. void item.notGuaranteed return eq(row.id, item.id) - }) + })) .where(({ item }) => { // @ts-expect-error Nullable generic refs expose only guaranteed fields. void item.notGuaranteed @@ -273,13 +269,13 @@ describe(`query API type algebra`, () => { const joinedBranchUnion = new Query() .unionAll(aRows, bRows) - .leftJoin({ row: rows }, ({ id: itemId, row }) => { + .join(leftJoin({ row: rows }, ({ id: itemId, row }) => { expectTypeOf>().toEqualTypeOf() expectTypeOf>().toEqualTypeOf() // @ts-expect-error The joined source declares no other field. void row.notGuaranteed return eq(itemId, row.id) - }) + })) .where((refs) => { void refs.id void refs.row.id @@ -293,7 +289,7 @@ describe(`query API type algebra`, () => { const rightJoinedBranchUnion = new Query() .unionAll(aRows, bRows) - .rightJoin({ row: rows }, ({ id: itemId, row }) => eq(itemId, row.id)) + .join(rightJoin({ row: rows }, ({ id: itemId, row }) => eq(itemId, row.id))) .select(({ id: itemId }) => ({ id: itemId })) return { @@ -522,7 +518,7 @@ describe(`query API type algebra`, () => { }, ) const rightJoined = union - .rightJoin({ row: rows }, ({ id, row }) => eq(id, row.id)) + .join(rightJoin({ row: rows }, ({ id, row }) => eq(id, row.id))) .select( ({ exactNull, @@ -539,7 +535,7 @@ describe(`query API type algebra`, () => { }), ) const fullJoined = union - .fullJoin({ row: rows }, ({ id, row }) => eq(id, row.id)) + .join(fullJoin({ row: rows }, ({ id, row }) => eq(id, row.id))) .select( ({ exactNull, diff --git a/packages/db/tests/query/query-api-type-algebra.test.ts b/packages/db/tests/query/query-api-type-algebra.test.ts index eb3025c8da..9430eaf65b 100644 --- a/packages/db/tests/query/query-api-type-algebra.test.ts +++ b/packages/db/tests/query/query-api-type-algebra.test.ts @@ -1,6 +1,6 @@ import { expect, test } from 'vitest' +import { createLiveQueryCollection, eq, leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' -import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { mockSyncCollectionOptions } from '../utils.js' test(`an unmatched whole-object projection publishes undefined`, () => { @@ -24,9 +24,8 @@ test(`an unmatched whole-object projection publishes undefined`, () => { query: (q) => q .from({ row: rows }) - .leftJoin({ department: departments }, ({ row, department }) => - eq(row.departmentId, department.id), - ) + .join(leftJoin({ department: departments }, ({ row, department }) => + eq(row.departmentId, department.id))) .select(({ department }) => ({ department })), }) @@ -69,15 +68,13 @@ test(`branch unions publish unmatched whole-object projections as undefined`, as const query = createLiveQueryCollection((q) => { const branchA = q .from({ rowA: rowsA }) - .leftJoin({ otherA: othersA }, ({ rowA, otherA }) => - eq(rowA.id, otherA.id), - ) + .join(leftJoin({ otherA: othersA }, ({ rowA, otherA }) => + eq(rowA.id, otherA.id))) .select(({ otherA }) => ({ other: otherA })) const branchB = q .from({ rowB: rowsB }) - .leftJoin({ otherB: othersB }, ({ rowB, otherB }) => - eq(rowB.id, otherB.id), - ) + .join(leftJoin({ otherB: othersB }, ({ rowB, otherB }) => + eq(rowB.id, otherB.id))) .select(({ otherB }) => ({ other: otherB })) return q.unionAll(branchA, branchB).select(({ other }) => ({ other })) diff --git a/packages/db/tests/query/query-once.test.ts b/packages/db/tests/query/query-once.test.ts index 74efc105e0..12a5af1d30 100644 --- a/packages/db/tests/query/query-once.test.ts +++ b/packages/db/tests/query/query-once.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { eq, innerJoin, queryOnce } from '../../src/query/index.js' import { CollectionImpl, createCollection } from '../../src/collection/index.js' -import { eq, queryOnce } from '../../src/query/index.js' import { Query } from '../../src/query/builder/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -233,11 +233,8 @@ describe(`queryOnce`, () => { const usersWithPosts = await queryOnce((q) => q .from({ user: usersCollection }) - .join( - { post: postsCollection }, - ({ user, post }) => eq(user.id, post.authorId), - `inner`, - ) + .join(innerJoin({ post: postsCollection }, + ({ user, post }) => eq(user.id, post.authorId))) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, diff --git a/packages/db/tests/query/query-while-syncing.test.ts b/packages/db/tests/query/query-while-syncing.test.ts index e7096ce5e3..3d9017d428 100644 --- a/packages/db/tests/query/query-while-syncing.test.ts +++ b/packages/db/tests/query/query-while-syncing.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { createLiveQueryCollection, eq, gt } from '../../src/query/index.js' +import { createLiveQueryCollection, eq, gt, innerJoin, leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createTransaction } from '../../src/transactions.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' @@ -317,11 +317,8 @@ describe(`Query while syncing`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `inner`, - ) + .join(innerJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -444,9 +441,8 @@ describe(`Query while syncing`, () => { query: (q) => q .from({ user: usersCollection }) - .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ) + .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -576,16 +572,10 @@ describe(`Query while syncing`, () => { query: (q) => q .from({ c1: collection1 }) - .join( - { c2: collection2 }, - ({ c1, c2 }) => eq(c1.id, c2.id), - `inner`, - ) - .join( - { c3: collection3 }, - ({ c1, c3 }) => eq(c1.id, c3.id), - `inner`, - ) + .join(innerJoin({ c2: collection2 }, + ({ c1, c2 }) => eq(c1.id, c2.id))) + .join(innerJoin({ c3: collection3 }, + ({ c1, c3 }) => eq(c1.id, c3.id))) .select(({ c1 }) => ({ id: c1.id, name: c1.name, @@ -918,11 +908,8 @@ describe(`Query while syncing`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - `inner`, - ) + .join(innerJoin({ dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id))) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, diff --git a/packages/db/tests/query/scheduler.test.ts b/packages/db/tests/query/scheduler.test.ts index 3d51a72dd5..a872ce9753 100644 --- a/packages/db/tests/query/scheduler.test.ts +++ b/packages/db/tests/query/scheduler.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' +import { createLiveQueryCollection, eq, fullJoin, innerJoin, isNull, leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' -import { createLiveQueryCollection, eq, isNull } from '../../src/query/index.js' import { createTransaction } from '../../src/transactions.js' import { createOptimisticAction } from '../../src/optimistic-action.js' import { @@ -103,7 +103,7 @@ function setupLiveQueryCollections(id: string) { query: (q) => q .from({ user: users }) - .join({ task: tasks }, ({ user, task }) => eq(user.id, task.userId)) + .join(leftJoin({ task: tasks }, ({ user, task }) => eq(user.id, task.userId))) .select(({ user, task }) => ({ userId: user.id, taskId: task.id, @@ -1081,11 +1081,8 @@ describe(`live query scheduler`, () => { ]) const query = new Query() .from({ left: leftQuery }) - .join( - { right: rightQuery ?? rightSource }, - ({ left, right }) => eq(left.id, right.id), - `inner`, - ) + .join(innerJoin({ right: rightQuery ?? rightSource }, + ({ left, right }) => eq(left.id, right.id))) .select(({ left, right }) => ({ id: left.id, left: left.value, @@ -1204,11 +1201,8 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ left: liveQueryA }) - .join( - { right: liveQueryB }, - ({ left, right }) => eq(left.id, right.id), - `full`, - ) + .join(fullJoin({ right: liveQueryB }, + ({ left, right }) => eq(left.id, right.id))) .select(({ left, right }) => ({ left: left.value, right: right.value, @@ -1296,11 +1290,8 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ left: liveQueryA }) - .join( - { right: collectionB }, - ({ left, right }) => eq(left.id, right.id), - `full`, - ) + .join(fullJoin({ right: collectionB }, + ({ left, right }) => eq(left.id, right.id))) .select(({ left, right }) => ({ left: left.value, right: right.value, @@ -1384,11 +1375,8 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ left: liveQueryA }) - .join( - { right: collectionB }, - ({ left, right }) => eq(left.id, right.id), - `full`, - ) + .join(fullJoin({ right: collectionB }, + ({ left, right }) => eq(left.id, right.id))) .select(({ left, right }) => ({ left: left.value, right: right.value, @@ -1815,9 +1803,8 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ account: accounts }) - .join({ user: users }, ({ user, account }) => - eq(user.id, account.user_id), - ) + .join(leftJoin({ user: users }, ({ user, account }) => + eq(user.id, account.user_id))) .select(({ account, user }) => ({ account: account, profile: user.profile, @@ -1840,9 +1827,8 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ accountWithUser: accountsWithUsers }) - .leftJoin({ team: activeTeams }, ({ accountWithUser, team }) => - eq(team.team.account_id, accountWithUser.account.id), - ) + .join(leftJoin({ team: activeTeams }, ({ accountWithUser, team }) => + eq(team.team.account_id, accountWithUser.account.id))) .select(({ accountWithUser, team }) => ({ account: accountWithUser.account, profile: accountWithUser.profile, @@ -1937,7 +1923,7 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ a: queryA }) - .leftJoin({ b: queryB }, ({ a, b }) => eq(a.id, b.id)) + .join(leftJoin({ b: queryB }, ({ a, b }) => eq(a.id, b.id))) .select(({ a, b }) => ({ id: a.id, aValue: a.value, diff --git a/packages/db/tests/query/select-spread.test.ts b/packages/db/tests/query/select-spread.test.ts index a902d52ff9..dca3df766a 100644 --- a/packages/db/tests/query/select-spread.test.ts +++ b/packages/db/tests/query/select-spread.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it } from 'vitest' +import { createLiveQueryCollection, innerJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' -import { createLiveQueryCollection } from '../../src/query/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' import { add, eq, upper } from '../../src/query/builder/functions.js' @@ -258,7 +258,7 @@ describe(`select spreads (runtime)`, () => { const collection = createLiveQueryCollection((q) => q .from({ m: messagesCollection }) - .join({ u: users }, ({ m, u }) => eq(m.id, u.id), `inner`) + .join(innerJoin({ u: users }, ({ m, u }) => eq(m.id, u.id))) .select(({ m, u }) => ({ ...m, ...u, diff --git a/packages/db/tests/query/subquery-user-value-oracle.test.ts b/packages/db/tests/query/subquery-user-value-oracle.test.ts index 5dc44ea6c4..d59515b25b 100644 --- a/packages/db/tests/query/subquery-user-value-oracle.test.ts +++ b/packages/db/tests/query/subquery-user-value-oracle.test.ts @@ -1,14 +1,13 @@ import { D2, MultiSet, output } from '@tanstack/db-ivm' import { describe, expect, it } from 'vitest' +import { count, + createLiveQueryCollection, + eq, + innerJoin, + materialize } from '../../src/query/index.js' import { buildQuery } from '../../src/query/builder/index.js' import { compileQuery } from '../../src/query/compiler/index.js' import { collectCollectionSources } from '../../src/query/ir.js' -import { - count, - createLiveQueryCollection, - eq, - materialize, -} from '../../src/query/index.js' import { withHistoryCleanup } from '../optimistic-history-oracle.js' import { stripVirtualProps } from '../utils.js' import { createControlledCollection } from './includes-oracle-helpers.js' @@ -82,9 +81,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .distinct() return q .from({ person: persons.collection }) - .innerJoin({ group: distinctGroups }, ({ person, group }) => - eq(person.groupId, group.groupId), - ) + .join(innerJoin({ group: distinctGroups }, ({ person, group }) => + eq(person.groupId, group.groupId))) .where(({ person }) => eq(person.enabled, true)) .select(({ person, group }) => ({ personId: person.id, @@ -176,9 +174,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .distinct() return q .from({ person: persons.collection }) - .innerJoin({ group: distinctGroups }, ({ person, group }) => - eq(person.groupId, group.groupId), - ) + .join(innerJoin({ group: distinctGroups }, ({ person, group }) => + eq(person.groupId, group.groupId))) .where(({ person }) => eq(person.enabled, true)) .select(({ person, group }) => ({ personId: person.id, @@ -408,9 +405,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .distinct() return q .from({ person: people.collection }) - .innerJoin({ group: groups }, ({ person, group }) => - eq(person.groupId, group.groupId), - ) + .join(innerJoin({ group: groups }, ({ person, group }) => + eq(person.groupId, group.groupId))) .where(({ group }) => eq(group.$key, 2)) .select(({ group }) => ({ groupId: group.groupId })) }, @@ -443,9 +439,8 @@ describe('subquery boundaries preserve operators and user rows', () => { })) return q .from({ person: people.collection }) - .innerJoin({ result: stats }, ({ person, result }) => - eq(person.id, result.stats.total), - ) + .join(innerJoin({ result: stats }, ({ person, result }) => + eq(person.id, result.stats.total))) .where(({ result }) => eq(result.$key, 'single_group')) .select(({ result }) => ({ total: result.stats.total })) }, @@ -481,9 +476,8 @@ describe('subquery boundaries preserve operators and user rows', () => { })) return q .from({ person: persons.collection }) - .innerJoin({ result: stats }, ({ person, result }) => - eq(person.id, result.stats.total), - ) + .join(innerJoin({ result: stats }, ({ person, result }) => + eq(person.id, result.stats.total))) .where(({ result }) => eq(result.$key, 'single_group')) .select(({ person, result }) => ({ personId: person.id, @@ -555,9 +549,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .select(({ item }) => ({ total: count(item.id) })) return q .from({ person: persons.collection }) - .innerJoin({ result: stats }, ({ person, result }) => - eq(person.id, result.total), - ) + .join(innerJoin({ result: stats }, ({ person, result }) => + eq(person.id, result.total))) .where(({ result }) => eq(result.$key, 'single_group')) .select(({ person, result }) => ({ personId: person.id, @@ -623,9 +616,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .findOne() return q .from({ anchor: anchors.collection }) - .innerJoin({ first: firstQuery }, ({ anchor, first: row }) => - eq(anchor.id, row.id), - ) + .join(innerJoin({ first: firstQuery }, ({ anchor, first: row }) => + eq(anchor.id, row.id))) .select(({ first: row }) => ({ id: row.id })) }, getKey: (row) => row.id, @@ -638,9 +630,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .findOne() return q .from({ first: firstQuery }) - .innerJoin({ anchor: anchors.collection }, ({ first: row, anchor }) => - eq(row.id, anchor.id), - ) + .join(innerJoin({ anchor: anchors.collection }, ({ first: row, anchor }) => + eq(row.id, anchor.id))) .select(({ first: row }) => ({ id: row.id })) }, getKey: (row) => row.id, @@ -699,9 +690,8 @@ describe('subquery boundaries preserve operators and user rows', () => { query: (q) => { const firstJoined = q .from({ person: persons.collection }) - .innerJoin({ tag: tagRows.collection }, ({ person, tag }) => - eq(person.groupId, tag.groupId), - ) + .join(innerJoin({ tag: tagRows.collection }, ({ person, tag }) => + eq(person.groupId, tag.groupId))) .orderBy(({ person }) => person.id, 'asc') .select(({ person, tag }) => ({ personId: person.id, @@ -710,9 +700,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .findOne() return q .from({ anchor: anchors.collection }) - .innerJoin({ first: firstJoined }, ({ anchor, first }) => - eq(anchor.id, first.personId), - ) + .join(innerJoin({ first: firstJoined }, ({ anchor, first }) => + eq(anchor.id, first.personId))) .select(({ anchor, first }) => ({ anchorId: anchor.id, tagId: first.tagId, @@ -814,9 +803,8 @@ describe('subquery boundaries preserve operators and user rows', () => { q .from({ person: persons.collection }) .where(({ person }) => eq(person.scope, anchor.id)) - .innerJoin({ tag: tagRows.collection }, ({ person, tag }) => - eq(person.groupId, tag.groupId), - ) + .join(innerJoin({ tag: tagRows.collection }, ({ person, tag }) => + eq(person.groupId, tag.groupId))) .orderBy(({ person }) => person.id, 'asc') .select(({ person, tag }) => ({ personId: person.id, diff --git a/packages/db/tests/query/subset-error-matrix.test.ts b/packages/db/tests/query/subset-error-matrix.test.ts index 8872aad5bc..0c476725ac 100644 --- a/packages/db/tests/query/subset-error-matrix.test.ts +++ b/packages/db/tests/query/subset-error-matrix.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' +import { createEffect, createLiveQueryCollection, eq, leftJoin } from '../../src/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' -import { createEffect, createLiveQueryCollection, eq } from '../../src/index.js' import { getLoadSubsetDemandKey } from '../../src/query/ir-stable-identity.js' import { captureMicrotaskErrors, mockSyncCollectionOptions } from '../utils.js' @@ -147,9 +147,8 @@ function startEffect( query: (q) => q .from({ item: primary }) - .leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId), - ), + .join(leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId))), ...callbacks, }) } @@ -176,9 +175,8 @@ function startLive( return createLiveQueryCollection((q) => q .from({ item: primary }) - .leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId), - ), + .join(leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId))), ) } return createLiveQueryCollection((q) => q.from({ item: primary })) @@ -431,9 +429,8 @@ describe(`loadSubset failure matrix`, () => { query: (q) => q .from({ item: parent }) - .leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId), - ), + .join(leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId))), onBatch: () => {}, onSourceError: (error) => sourceErrors.push(error), }) @@ -443,9 +440,8 @@ describe(`loadSubset failure matrix`, () => { ? createLiveQueryCollection((q) => q .from({ item: parent }) - .leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId), - ), + .join(leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId))), ) : undefined @@ -509,9 +505,8 @@ describe(`loadSubset failure matrix`, () => { const live = createLiveQueryCollection((q) => q .from({ item: parent }) - .leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId), - ), + .join(leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId))), ) const originalQueueMicrotask = globalThis.queueMicrotask const queuedMicrotasks: Array<() => void> = [] diff --git a/packages/db/tests/query/union-all.test-d.ts b/packages/db/tests/query/union-all.test-d.ts index 16001ef210..45e0a73690 100644 --- a/packages/db/tests/query/union-all.test-d.ts +++ b/packages/db/tests/query/union-all.test-d.ts @@ -1,12 +1,11 @@ import { describe, expectTypeOf, test } from 'vitest' -import { createCollection } from '../../src/collection/index.js' -import { - coalesce, - count, - createLiveQueryCollection, - eq, +import { coalesce, count, createLiveQueryCollection, eq, + fullJoin, gt, -} from '../../src/query/index.js' + innerJoin, + leftJoin, + rightJoin } from '../../src/query/index.js' +import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' import type { @@ -332,11 +331,8 @@ describe(`unionAll types`, () => { return q .unionAll(messageRows, toolCallRows) - .join( - { user: users }, - ({ userId, user }) => eq(userId, user.id), - `inner`, - ) + .join(innerJoin({ user: users }, + ({ userId, user }) => eq(userId, user.id))) .select(({ kind, id, user }) => ({ kind, id, @@ -370,11 +366,8 @@ describe(`unionAll types`, () => { const collection = createLiveQueryCollection((q) => { const messagesWithUsers = q .from({ message: messages }) - .join( - { user: users }, - ({ message, user }) => eq(message.userId, user.id), - `inner`, - ) + .join(innerJoin({ user: users }, + ({ message, user }) => eq(message.userId, user.id))) const toolCallRows = q.from({ toolCall: toolCalls }) return q.unionAll(messagesWithUsers, toolCallRows) @@ -401,11 +394,8 @@ describe(`unionAll types`, () => { const collection = createLiveQueryCollection((q) => { const messagesWithUsers = q .from({ message: messages }) - .join( - { user: users }, - ({ message, user }) => eq(message.userId, user.id), - `inner`, - ) + .join(innerJoin({ user: users }, + ({ message, user }) => eq(message.userId, user.id))) .select(({ message, user }) => ({ id: message.id, timestamp: message.timestamp, @@ -449,9 +439,8 @@ describe(`unionAll types`, () => { message: messages, toolCall: toolCalls, }) - .leftJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ), + .join(leftJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id))), ) expectTypeOf(collection.toArray).toMatchTypeOf< @@ -483,9 +472,8 @@ describe(`unionAll types`, () => { message: messages, toolCall: toolCalls, }) - .rightJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ), + .join(rightJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id))), ) expectTypeOf(collection.toArray).toMatchTypeOf< @@ -510,9 +498,8 @@ describe(`unionAll types`, () => { message: messages, toolCall: toolCalls, }) - .fullJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ), + .join(fullJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id))), ) expectTypeOf(collection.toArray).toMatchTypeOf< @@ -561,7 +548,7 @@ describe(`unionAll types`, () => { return q .unionAll(messageRows, toolCallRows) - .rightJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)) + .join(rightJoin({ user: users }, ({ userId, user }) => eq(userId, user.id))) .select(({ kind, id, user }) => ({ kind, id, @@ -603,7 +590,7 @@ describe(`unionAll types`, () => { return q .unionAll(messageRows, toolCallRows) - .fullJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)) + .join(fullJoin({ user: users }, ({ userId, user }) => eq(userId, user.id))) .select(({ kind, id, user }) => ({ kind, id, diff --git a/packages/db/tests/query/union-all.test.ts b/packages/db/tests/query/union-all.test.ts index ce5f0faf51..388a075d4f 100644 --- a/packages/db/tests/query/union-all.test.ts +++ b/packages/db/tests/query/union-all.test.ts @@ -1,16 +1,15 @@ import { describe, expect, it } from 'vitest' -import { createCollection } from '../../src/collection/index.js' -import { BTreeIndex } from '../../src/indexes/btree-index.js' -import { - caseWhen, - coalesce, - concat, - count, +import { caseWhen, coalesce, concat, count, createLiveQueryCollection, eq, + fullJoin, gt, - toArray, -} from '../../src/query/index.js' + innerJoin, + leftJoin, + rightJoin, + toArray } from '../../src/query/index.js' +import { createCollection } from '../../src/collection/index.js' +import { BTreeIndex } from '../../src/indexes/btree-index.js' import { flushPromises, mockSyncCollectionOptions, @@ -371,11 +370,8 @@ describe(`unionAll`, () => { return q .unionAll(messageRows, toolCallRows) - .join( - { user: users }, - ({ userId, user }) => eq(userId, user.id), - `inner`, - ) + .join(innerJoin({ user: users }, + ({ userId, user }) => eq(userId, user.id))) .select(({ kind, id, user }) => ({ kind, id, @@ -428,11 +424,8 @@ describe(`unionAll`, () => { return q .unionAll(messageRows, toolCallRows) - .join( - { user: users }, - ({ userId, user }) => eq(userId, user.id), - `inner`, - ) + .join(innerJoin({ user: users }, + ({ userId, user }) => eq(userId, user.id))) .select(({ kind, id, user }) => ({ kind, id, @@ -918,11 +911,8 @@ describe(`unionAll`, () => { const collection = createLiveQueryCollection((q) => { const visibleMessagesWithUsers = q .from({ message: messages }) - .join( - { user: users }, - ({ message, user }) => eq(message.userId, user.id), - `inner`, - ) + .join(innerJoin({ user: users }, + ({ message, user }) => eq(message.userId, user.id))) .where(({ message }) => eq(message.kind, `visible`)) .select(({ message, user }) => ({ id: message.id, @@ -974,12 +964,9 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .join( - { user: users }, + .join(innerJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - `inner`, - ) + eq(coalesce(message.userId, toolCall.userId), user.id))) .select(({ message, toolCall, user }) => ({ messageId: message.id, toolCallId: toolCall.id, @@ -1011,9 +998,8 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .leftJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ) + .join(leftJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id))) .select(({ message, toolCall, user }) => ({ eventId: coalesce(message.id, toolCall.id), userName: user.name, @@ -1047,9 +1033,8 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .rightJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ) + .join(rightJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id))) .orderBy(({ user }) => user.id), ) @@ -1087,9 +1072,8 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .fullJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ) + .join(fullJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id))) .orderBy(({ message, toolCall, user }) => coalesce(message.timestamp, toolCall.timestamp, user.id), ), @@ -1133,12 +1117,9 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .join( - { user: users }, + .join(innerJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - `inner`, - ) + eq(coalesce(message.userId, toolCall.userId), user.id))) .select(({ message, toolCall, user }) => ({ id: coalesce(message.id, toolCall.id), userName: user.name, @@ -1179,9 +1160,8 @@ describe(`unionAll`, () => { return q .from({ user: users }) - .leftJoin({ event: events }, ({ user, event }) => - eq(user.id, event.userId), - ) + .join(leftJoin({ event: events }, ({ user, event }) => + eq(user.id, event.userId))) .select(({ user, event }) => ({ userId: user.id, eventTimestamp: event.timestamp, @@ -1232,9 +1212,8 @@ describe(`unionAll`, () => { return q .from({ user: users }) - .leftJoin({ event: events }, ({ user, event }) => - eq(user.name, event.label), - ) + .join(leftJoin({ event: events }, ({ user, event }) => + eq(user.name, event.label))) .select(({ user, event }) => ({ userId: user.id, label: event.label, @@ -1291,9 +1270,8 @@ describe(`unionAll`, () => { return q .from({ user: users }) - .leftJoin({ event: events }, ({ user, event }) => - eq(user.id, event.userId), - ) + .join(leftJoin({ event: events }, ({ user, event }) => + eq(user.id, event.userId))) .select(({ user, event }) => ({ userId: user.id, eventTimestamp: event.timestamp, @@ -1398,9 +1376,8 @@ describe(`unionAll`, () => { createLiveQueryCollection((q) => q .from({ message: messages }) - .join({ toolCall: toolCalls, user: users }, ({ message, toolCall }) => - eq(message.id, toolCall.id), - ), + .join(leftJoin({ toolCall: toolCalls, user: users }, ({ message, toolCall }) => + eq(message.id, toolCall.id))), ), ).toThrow(OnlyOneSourceAllowedError) }) diff --git a/packages/db/tests/query/validate-aliases.test.ts b/packages/db/tests/query/validate-aliases.test.ts index c25036d9df..5ac8873997 100644 --- a/packages/db/tests/query/validate-aliases.test.ts +++ b/packages/db/tests/query/validate-aliases.test.ts @@ -1,9 +1,8 @@ import { beforeEach, describe, expect, test } from 'vitest' -import { - createLiveQueryCollection, - eq, - toArray, -} from '../../src/query/index.js' +import { createLiveQueryCollection, eq, + innerJoin, + leftJoin, + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -64,9 +63,8 @@ describe(`Alias validation in subqueries`, () => { query: (q) => { const locksAgg = q .from({ lock: locksCollection }) - .join({ vote: votesCollection }, ({ lock, vote }) => - eq(lock._id, vote.lockId), - ) + .join(leftJoin({ vote: votesCollection }, ({ lock, vote }) => + eq(lock._id, vote.lockId))) .select(({ lock }) => ({ _id: lock._id, lockName: lock.name, @@ -74,9 +72,8 @@ describe(`Alias validation in subqueries`, () => { return q .from({ vote: votesCollection }) - .join({ lock: locksAgg }, ({ vote, lock }) => - eq(lock._id, vote.lockId), - ) + .join(leftJoin({ lock: locksAgg }, ({ vote, lock }) => + eq(lock._id, vote.lockId))) .select(({ vote, lock }) => ({ voteId: vote._id, lockName: lock.lockName, @@ -101,9 +98,8 @@ describe(`Alias validation in subqueries`, () => { .select(({ item }) => ({ id: item._id })) return q .from({ item: locksCollection }) - .innerJoin({ child: reusable }, ({ item, child }) => - eq(item._id, child.id), - ) + .join(innerJoin({ child: reusable }, ({ item, child }) => + eq(item._id, child.id))) .select(({ item }) => ({ id: item._id })) }, }) @@ -121,12 +117,10 @@ describe(`Alias validation in subqueries`, () => { query: (q) => q .from({ lock: locksCollection }) - .join({ vote: votesCollection }, ({ lock, vote }) => - eq(vote.lockId, lock._id), - ) - .join({ vote: votesCollection }, ({ lock, vote }) => - eq(vote.lockId, lock._id), - ) + .join(leftJoin({ vote: votesCollection }, ({ lock, vote }) => + eq(vote.lockId, lock._id))) + .join(leftJoin({ vote: votesCollection }, ({ lock, vote }) => + eq(vote.lockId, lock._id))) .select(({ lock }) => ({ _id: lock._id })), }) }).toThrow(/alias "vote" more than once/) @@ -164,9 +158,8 @@ describe(`Alias validation in subqueries`, () => { .from({ tool: votesCollection }) .select(({ tool }) => ({ rid: tool._id })), ) - .innerJoin({ anchor: locksCollection }, ({ rid, anchor }) => - eq(rid, anchor._id), - ) + .join(innerJoin({ anchor: locksCollection }, ({ rid, anchor }) => + eq(rid, anchor._id))) .select(({ rid, anchor }) => ({ rid, votes: toArray( @@ -212,9 +205,8 @@ describe(`Alias validation in subqueries`, () => { .from({ lock: locksCollection }) .select(({ lock }) => ({ id: lock._id })), ) - .innerJoin({ vote: votesCollection }, ({ id, vote }) => - eq(vote._id, id), - ) + .join(innerJoin({ vote: votesCollection }, ({ id, vote }) => + eq(vote._id, id))) .select(({ id, vote }) => ({ id, lockId: vote.lockId })), }) await live.preload() @@ -261,10 +253,8 @@ describe(`Alias validation in subqueries`, () => { query: (q) => { const locksAgg = q .from({ lock: locksCollection }) - .join( - { v: votesCollection }, // Uses unique alias "v" instead of "vote" - ({ lock, v }) => eq(lock._id, v.lockId), - ) + .join(leftJoin({ v: votesCollection }, // Uses unique alias "v" instead of "vote" + ({ lock, v }) => eq(lock._id, v.lockId))) .select(({ lock }) => ({ _id: lock._id, lockName: lock.name, @@ -272,9 +262,8 @@ describe(`Alias validation in subqueries`, () => { return q .from({ vote: votesCollection }) - .join({ lock: locksAgg }, ({ vote, lock }) => - eq(lock._id, vote.lockId), - ) + .join(leftJoin({ lock: locksAgg }, ({ vote, lock }) => + eq(lock._id, vote.lockId))) .select(({ vote, lock }) => ({ voteId: vote._id, lockName: lock.lockName, diff --git a/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts b/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts index 135bf61c33..2681f7a307 100644 --- a/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts +++ b/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts @@ -1,3 +1,9 @@ +import { describe, expectTypeOf, test } from 'vitest' +import { createLiveQueryCollection, + eq, + leftJoin, + materialize, + toArray } from '../../src/query/index.js' /** * # Which types represent virtual-field-bearing rows? * @@ -33,14 +39,7 @@ * nested-helper cells and `GetRawResult` enrichment breaks eleven * projected-value cells. */ -import { describe, expectTypeOf, test } from 'vitest' import { createCollection } from '../../src/collection/index.js' -import { - createLiveQueryCollection, - eq, - materialize, - toArray, -} from '../../src/query/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { RefLeaf } from '../../src/query/builder/types.js' import type { SingleRowRefProxy } from '../../src/query/builder/ref-proxy.js' @@ -227,9 +226,8 @@ describe(`virtual row field type boundary`, () => { unmatchedRows: toArray( q .from({ child: rows }) - .leftJoin({ missing: rows }, ({ child, missing }) => - eq(child.id, missing.id), - ) + .join(leftJoin({ missing: rows }, ({ child, missing }) => + eq(child.id, missing.id))) .select(({ missing }) => missing), ), objects: toArray( diff --git a/packages/db/tests/query/virtual-row-fields-oracle.test.ts b/packages/db/tests/query/virtual-row-fields-oracle.test.ts index 93b54f08ed..d77bd207be 100644 --- a/packages/db/tests/query/virtual-row-fields-oracle.test.ts +++ b/packages/db/tests/query/virtual-row-fields-oracle.test.ts @@ -1,3 +1,9 @@ +import { describe, expect, test } from 'vitest' +import { createLiveQueryCollection, + eq, + leftJoin, + materialize, + toArray } from '../../src/query/index.js' /** * # Which published values carry virtual row fields? * @@ -32,14 +38,7 @@ * observations. `checkWithCleanup` retains the primary mismatch as the cause * when Collection cleanup also fails and attempts every cleanup. */ -import { describe, expect, test } from 'vitest' import { createCollection } from '../../src/collection/index.js' -import { - createLiveQueryCollection, - eq, - materialize, - toArray, -} from '../../src/query/index.js' import { hasVirtualProps } from '../../src/virtual-props.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -282,9 +281,8 @@ describe(`virtual row field runtime boundary`, () => { q .from({ child: rows }) .where(({ child }) => eq(child.id, row.id)) - .leftJoin({ missing: missingRows }, ({ child, missing }) => - eq(child.id, missing.id), - ) + .join(leftJoin({ missing: missingRows }, ({ child, missing }) => + eq(child.id, missing.id))) .select(({ missing }) => missing), ), objects: toArray( From 16c0be59338270c4810977bb82a20e946c3e8140 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 15:08:45 -0600 Subject: [PATCH 02/32] feat(db): name the missing join operator when .join() gets the old call shape Co-authored-by: Isaac --- docs/errors.md | 12 +++++++++++- packages/db/src/errors.ts | 12 +++++++++++- packages/db/src/query/builder/index.ts | 11 +++++++++++ packages/db/tests/error-sample-arguments.ts | 1 + packages/db/tests/fixtures/error-codes.json | 3 ++- packages/db/tests/fixtures/error-messages.json | 5 ++++- packages/db/tests/query/builder/join.test.ts | 11 +++++++++++ 7 files changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/errors.md b/docs/errors.md index fcfdcce0c4..be5f1ddfef 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -490,7 +490,7 @@ Invalid source for from clause: Expected an object with a single key-value pair ``` ```text -Invalid source for join clause: Expected an object with a single key-value pair like { alias: collection }. For example: .join({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id)). Got: number +Invalid source for join clause: Expected an object with a single key-value pair like { alias: collection }. For example: .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Got: number ``` ```text @@ -996,6 +996,16 @@ These are plain `Error`, `TypeError`, `RangeError`, or `AggregateError` values, and `console` messages about runtime failures. Each heading names the source file under `packages/db/src` that writes it. + + +## Error 245: `JoinOperatorRequiredError` + +Development builds show the full message. For example: + +```text +.join() takes a join operator, such as .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Import leftJoin, innerJoin, rightJoin, fullJoin, or joinOf from @tanstack/db. Got: object +``` + ## Error 88: `client.ts` diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index 7ced1918a7..140232b96d 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -610,6 +610,16 @@ export class InvalidSourceError extends QueryBuilderError { } } +export class JoinOperatorRequiredError extends QueryBuilderError { + constructor(type: string) { + super( + devBuild() && process.env.NODE_ENV !== `production` + ? `.join() takes a join operator, such as .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Import leftJoin, innerJoin, rightJoin, fullJoin, or joinOf from @tanstack/db. Got: ${type}` + : codedMessage(245, { type }), + ) + } +} + export type SourceClauseContext = `from clause` | `unionAll clause` | `join clause` @@ -626,7 +636,7 @@ export class InvalidSourceTypeError extends QueryBuilderError { context === `unionAll clause` ? `.unionAll({ todos: todosCollection, events: eventsCollection })` : context === `join clause` - ? `.join({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))` + ? `.join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id)))` : `.from({ todos: todosCollection })` return ( `Invalid source for ${context}: Expected ${expected}. ` + diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index ec524a09cb..b5e33c3d49 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -21,6 +21,7 @@ import { InvalidSourceError, InvalidSourceTypeError, InvalidWhereExpressionError, + JoinOperatorRequiredError, OnlyOneSourceAllowedError, QueryMustHaveFromClauseError, SubQueryMustHaveFromClauseError, @@ -338,6 +339,16 @@ export class BaseQueryBuilder { ): QueryBuilder< MergeContextWithJoinType, TJoinType> > { + // Untyped callers can still pass the removed (source, on, type) shape. + const value: unknown = operator + if ( + typeof value !== `object` || + value === null || + typeof (value as { compile?: unknown }).compile !== `function` + ) + throw new JoinOperatorRequiredError( + value === null ? `null` : typeof value, + ) const [alias, from] = this._createRefForSource( operator.source, `join clause`, diff --git a/packages/db/tests/error-sample-arguments.ts b/packages/db/tests/error-sample-arguments.ts index 0f658be69b..b72fb20bc9 100644 --- a/packages/db/tests/error-sample-arguments.ts +++ b/packages/db/tests/error-sample-arguments.ts @@ -91,6 +91,7 @@ export const errorSampleArguments: Record>> = { ['join clause', 'number'], ['unionAll clause', 'undefined'], ], + JoinOperatorRequiredError: [['object']], JoinConditionMustBeEqualityError: [[]], QueryMustHaveFromClauseError: [[]], InvalidWhereExpressionError: [['boolean']], diff --git a/packages/db/tests/fixtures/error-codes.json b/packages/db/tests/fixtures/error-codes.json index c2f38a1bc9..27197cea5d 100644 --- a/packages/db/tests/fixtures/error-codes.json +++ b/packages/db/tests/fixtures/error-codes.json @@ -90,5 +90,6 @@ "NameRequiredError": 177, "GetKeyRequiredError": 178, "DuplicateTransactionIdError": 233, - "SetWindowFindOneLimitError": 244 + "SetWindowFindOneLimitError": 244, + "JoinOperatorRequiredError": 245 } diff --git a/packages/db/tests/fixtures/error-messages.json b/packages/db/tests/fixtures/error-messages.json index 0f3054450a..81fd2f071c 100644 --- a/packages/db/tests/fixtures/error-messages.json +++ b/packages/db/tests/fixtures/error-messages.json @@ -129,7 +129,7 @@ ], "InvalidSourceTypeError": [ "Invalid source for from clause: Expected an object with a single key-value pair like { alias: collection }. For example: .from({ todos: todosCollection }). Got: string", - "Invalid source for join clause: Expected an object with a single key-value pair like { alias: collection }. For example: .join({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id)). Got: number", + "Invalid source for join clause: Expected an object with a single key-value pair like { alias: collection }. For example: .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Got: number", "Invalid source for unionAll clause: Expected an object with one or more key-value pairs like { alias: collection }. For example: .unionAll({ todos: todosCollection, events: eventsCollection }). Got: undefined" ], "JoinConditionMustBeEqualityError": [ @@ -260,5 +260,8 @@ ], "SetWindowFindOneLimitError": [ "setWindow() cannot change the limit of a findOne() query." + ], + "JoinOperatorRequiredError": [ + ".join() takes a join operator, such as .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Import leftJoin, innerJoin, rightJoin, fullJoin, or joinOf from @tanstack/db. Got: object" ] } diff --git a/packages/db/tests/query/builder/join.test.ts b/packages/db/tests/query/builder/join.test.ts index 25a06dea65..716a8c1784 100644 --- a/packages/db/tests/query/builder/join.test.ts +++ b/packages/db/tests/query/builder/join.test.ts @@ -3,6 +3,7 @@ import { fullJoin, innerJoin, leftJoin, rightJoin } from '@tanstack/db' import { CollectionImpl } from '../../../src/collection/index.js' import { Query, getQueryIR } from '../../../src/query/builder/index.js' import { and, eq, gt } from '../../../src/query/builder/functions.js' +import { JoinOperatorRequiredError } from '../../../src/errors.js' // Test schema interface Employee { @@ -360,3 +361,13 @@ describe(`QueryBuilder.join`, () => { }) }) }) + +describe(`join operator values`, () => { + it(`rejects the removed source-and-callback call shape with a coded error`, () => { + const builder = new Query().from({ employees: employeesCollection }) as any + expect(() => + builder.join({ departments: departmentsCollection }, () => true), + ).toThrow(JoinOperatorRequiredError) + expect(() => builder.join(undefined)).toThrow(/Got: undefined/) + }) +}) From 92ad30e633a4c01c0177eb8a0e7ce65abfb20bc4 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 15:25:31 -0600 Subject: [PATCH 03/32] feat(db): carry grouping on aggregates and groupKeys Aggregate values (count, sum, avg, min, max) carry the grouping compiler. A groupBy with no aggregate takes groupKeys(...); without it, compilation throws GroupingOperatorRequiredError. orderBy rewrites aggregates only when it finds one. Co-authored-by: Isaac --- docs/errors.md | 10 + packages/db/src/errors.ts | 10 + packages/db/src/query/builder/functions.ts | 11 +- packages/db/src/query/builder/group-keys.ts | 18 + packages/db/src/query/builder/index.ts | 7 +- .../db/src/query/compiler/find-aggregate.ts | 23 ++ packages/db/src/query/compiler/group-by.ts | 6 +- packages/db/src/query/compiler/grouping.ts | 9 + packages/db/src/query/compiler/index.ts | 17 +- packages/db/src/query/compiler/order-by.ts | 18 +- packages/db/src/query/index.ts | 3 + packages/db/src/query/ir.ts | 5 + packages/db/tests/error-sample-arguments.ts | 1 + packages/db/tests/fixtures/error-codes.json | 3 +- .../db/tests/fixtures/error-messages.json | 3 + .../aggregate-select-oracle.property.test.ts | 15 +- .../query/aggregate-value-contracts.test-d.ts | 9 +- .../tests/query/async-hold-outcomes.test.ts | 20 +- .../db/tests/query/builder/ref-proxy.test.ts | 3 +- packages/db/tests/query/case-when.test-d.ts | 13 +- packages/db/tests/query/case-when.test.ts | 22 +- packages/db/tests/query/clone-query.test.ts | 3 +- .../cold-join-reconciliation-oracle.test.ts | 66 ++-- .../collection-ref-compatibility.test.ts | 4 +- .../query/compiler/group-by-pipeline.test.ts | 3 +- .../db/tests/query/compiler/group-by.test.ts | 5 +- .../db/tests/query/compiler/select.test.ts | 13 +- packages/db/tests/query/composables.test.ts | 20 +- packages/db/tests/query/distinct.test.ts | 16 +- .../db/tests/query/findone-joins.test-d.ts | 88 +++-- .../tests/query/functional-variants.test-d.ts | 34 +- .../tests/query/functional-variants.test.ts | 20 +- .../tests/query/generic-collection.test-d.ts | 13 +- packages/db/tests/query/group-by.test.ts | 80 ++++- .../identity-output-shape-oracle.test.ts | 34 +- .../includes-alias-shadowing-oracle.test.ts | 324 ++++++++++++------ ...ncludes-collection-oracle.property.test.ts | 21 +- .../includes-context-transport-oracle.test.ts | 237 ++++++++----- ...-cross-formulation-oracle.property.test.ts | 13 +- .../tests/query/includes-lazy-loading.test.ts | 13 +- ...ludes-outer-join-route-work-oracle.test.ts | 15 +- .../query/includes-publication-oracle.test.ts | 16 +- .../query/includes-query-shape-oracle.test.ts | 22 +- .../query/includes-scope-identity-oracle.ts | 54 ++- .../query/includes-temporal-oracle.test.ts | 10 +- .../includes-work-counter-oracle.test.ts | 23 +- packages/db/tests/query/includes.test.ts | 16 +- packages/db/tests/query/indexes.test.ts | 56 ++- .../query/ir-stable-identity-oracle.test.ts | 31 +- .../join-result-key-oracle.property.test.ts | 2 +- .../db/tests/query/join-subquery.test-d.ts | 67 +++- packages/db/tests/query/join-subquery.test.ts | 255 ++++++++++---- packages/db/tests/query/join.test-d.ts | 247 +++++++++---- packages/db/tests/query/join.test.ts | 310 ++++++++++++----- .../tests/query/live-query-collection.test.ts | 92 +++-- .../query/load-subset-join-dedupe.test.ts | 13 +- ...ad-subset-replay-refinement-oracle.test.ts | 31 +- ...source-readiness-refinement-oracle.test.ts | 9 +- .../tests/query/load-subset-subquery.test.ts | 31 +- .../query/optimizer-semantics-oracle.test.ts | 95 +++-- packages/db/tests/query/optimizer.test.ts | 167 ++++++--- .../query/optional-fields-negative.test-d.ts | 22 +- .../query/optional-fields-runtime.test.ts | 14 +- packages/db/tests/query/order-by.test.ts | 33 +- .../ordered-lifecycle-oracle.property.test.ts | 32 +- .../ordered-work-oracle.property.test.ts | 132 +++++-- .../query/pagination-oracle.property.test.ts | 30 +- .../query/query-api-type-algebra.test-d.ts | 69 ++-- .../query/query-api-type-algebra.test.ts | 27 +- packages/db/tests/query/query-once.test.ts | 9 +- .../tests/query/query-while-syncing.test.ts | 43 ++- packages/db/tests/query/scheduler.test.ts | 57 ++- packages/db/tests/query/select-spread.test.ts | 2 +- .../query/subquery-user-value-oracle.test.ts | 84 +++-- .../tests/query/subset-error-matrix.test.ts | 42 ++- packages/db/tests/query/union-all.test-d.ts | 57 ++- packages/db/tests/query/union-all.test.ts | 92 +++-- .../db/tests/query/validate-aliases.test.ts | 71 ++-- .../query/virtual-row-fields-oracle.test-d.ts | 13 +- .../query/virtual-row-fields-oracle.test.ts | 13 +- 80 files changed, 2623 insertions(+), 1014 deletions(-) create mode 100644 packages/db/src/query/builder/group-keys.ts create mode 100644 packages/db/src/query/compiler/find-aggregate.ts create mode 100644 packages/db/src/query/compiler/grouping.ts diff --git a/docs/errors.md b/docs/errors.md index be5f1ddfef..f3d2a9d261 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -2479,3 +2479,13 @@ Development builds show this message, with `${...}` replaced by values: ```text A windowed or findOne result row has no include route ``` + + + +## Error 246: `GroupingOperatorRequiredError` + +Development builds show the full message. For example: + +```text +groupBy() without an aggregate needs groupKeys(), such as .groupBy(groupKeys(({ todo }) => todo.status)). Import groupKeys from @tanstack/db. A query with an aggregate such as count() needs nothing extra. +``` diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index 140232b96d..99473f6454 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -726,6 +726,16 @@ export class FnSelectWithGroupByError extends QueryCompilationError { } } +export class GroupingOperatorRequiredError extends QueryCompilationError { + constructor() { + super( + devBuild() && process.env.NODE_ENV !== `production` + ? `groupBy() without an aggregate needs groupKeys(), such as .groupBy(groupKeys(({ todo }) => todo.status)). Import groupKeys from @tanstack/db. A query with an aggregate such as count() needs nothing extra.` + : codedMessage(246), + ) + } +} + export class UnsupportedFnSelectResultError extends QueryCompilationError { constructor(valueDescription: string) { super( diff --git a/packages/db/src/query/builder/functions.ts b/packages/db/src/query/builder/functions.ts index c25c0bf772..c313d90cfa 100644 --- a/packages/db/src/query/builder/functions.ts +++ b/packages/db/src/query/builder/functions.ts @@ -1,5 +1,6 @@ import { Aggregate, Func, isBasicOrAggregateExpression } from '../ir' import { codedMessage, devBuild } from '../../error-message.js' +import { grouping } from '../compiler/grouping.js' import { isRefProxy, toExpression } from './ref-proxy.js' import { registerWrapper } from './wrapper-identity.js' import type { BasicExpression } from '../ir' @@ -658,7 +659,7 @@ export function divide( // Aggregates export function count(arg: ExpressionLike): Aggregate { - return new Aggregate(`count`, [toExpression(arg)]) + return new Aggregate(`count`, [toExpression(arg)], grouping) } export function avg(arg: T): Aggregate @@ -669,7 +670,7 @@ export function avg( arg: NumericAggregateArgument, ): Aggregate export function avg(arg: ExpressionLike): Aggregate { - return new Aggregate(`avg`, [toExpression(arg)]) + return new Aggregate(`avg`, [toExpression(arg)], grouping) } export function sum(arg: T): Aggregate @@ -680,7 +681,7 @@ export function sum( arg: NumericAggregateArgument, ): Aggregate export function sum(arg: ExpressionLike): Aggregate { - return new Aggregate(`sum`, [toExpression(arg)]) + return new Aggregate(`sum`, [toExpression(arg)], grouping) } export function min(arg: T): Aggregate @@ -689,7 +690,7 @@ export function min( arg: OrderableAggregateArgument, ): Aggregate> export function min(arg: ExpressionLike): Aggregate { - return new Aggregate(`min`, [toExpression(arg)]) + return new Aggregate(`min`, [toExpression(arg)], grouping) } export function max(arg: T): Aggregate @@ -698,7 +699,7 @@ export function max( arg: OrderableAggregateArgument, ): Aggregate> export function max(arg: ExpressionLike): Aggregate { - return new Aggregate(`max`, [toExpression(arg)]) + return new Aggregate(`max`, [toExpression(arg)], grouping) } /** diff --git a/packages/db/src/query/builder/group-keys.ts b/packages/db/src/query/builder/group-keys.ts new file mode 100644 index 0000000000..9f20359b94 --- /dev/null +++ b/packages/db/src/query/builder/group-keys.ts @@ -0,0 +1,18 @@ +import { grouping } from '../compiler/grouping.js' +import type { Grouping } from '../compiler/grouping.js' +import type { Context, GroupByCallback } from './types.js' + +/** + * Group keys for `query.groupBy(...)` when the query has no aggregate. + * Aggregates such as `count` bring the grouping compiler themselves. + */ +export interface GroupKeys { + callback: GroupByCallback + grouping: Grouping +} + +export function groupKeys( + callback: GroupByCallback, +): GroupKeys { + return { callback, grouping } +} diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index b5e33c3d49..7b608815d6 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -43,6 +43,7 @@ import { } from './functions.js' import type { CollectionResolver } from './clone-query.js' import type { JoinOperator, JoinType } from './join-operators.js' +import type { GroupKeys } from './group-keys.js' import type { SourceClauseContext } from '../../errors.js' import type { NamespacedRow, SingleResult } from '../../types.js' import type { @@ -687,7 +688,10 @@ export class BaseQueryBuilder { * })) * ``` */ - groupBy(callback: GroupByCallback): QueryBuilder { + groupBy( + keys: GroupByCallback | GroupKeys, + ): QueryBuilder { + const callback = typeof keys === `function` ? keys : keys.callback const aliases = this._getCurrentAliases() const refProxy = createRefProxy( aliases, @@ -704,6 +708,7 @@ export class BaseQueryBuilder { return this._clone({ ...this.query, groupBy: [...existingGroupBy, ...newExpressions], + ...(typeof keys === `function` ? {} : { grouping: keys.grouping }), }) as any } diff --git a/packages/db/src/query/compiler/find-aggregate.ts b/packages/db/src/query/compiler/find-aggregate.ts new file mode 100644 index 0000000000..0df3036f17 --- /dev/null +++ b/packages/db/src/query/compiler/find-aggregate.ts @@ -0,0 +1,23 @@ +import { Aggregate, ConditionalSelect, Func, isExpressionLike } from '../ir.js' + +/** + * Returns the first aggregate in a select, having, or orderBy value. It does + * not enter include subqueries, which compile with their own aggregates. + * A plain object is a nested select; its fields may use any names. + */ +export function findAggregate(value: unknown): Aggregate | undefined { + if (value === null || typeof value !== `object`) return undefined + if (Array.isArray(value)) { + for (const item of value) { + const aggregate = findAggregate(item) + if (aggregate) return aggregate + } + return undefined + } + if (value instanceof Aggregate) return value + if (value instanceof Func) return findAggregate(value.args) + if (value instanceof ConditionalSelect) + return findAggregate([value.branches, value.defaultValue]) + if (isExpressionLike(value)) return undefined + return findAggregate(Object.values(value)) +} diff --git a/packages/db/src/query/compiler/group-by.ts b/packages/db/src/query/compiler/group-by.ts index 528dd30d17..05ef0aa755 100644 --- a/packages/db/src/query/compiler/group-by.ts +++ b/packages/db/src/query/compiler/group-by.ts @@ -287,8 +287,6 @@ function getRowVirtualMetadata(row: NamespacedRow): RowVirtualMetadata { } } -const { sum, count, avg, min, max } = groupByOperators - /** * Processes the GROUP BY clause with optional HAVING and SELECT * Works with the new $selected structure from early SELECT processing @@ -732,6 +730,10 @@ function getAggregateFunction(aggExpr: Aggregate) { return compiledExpr(namespacedRow) } + // Read the operators here: a top-level destructure counts as a side + // effect and would keep them in bundles that never aggregate. + const { sum, count, avg, min, max } = groupByOperators + // Return the appropriate aggregate function switch (aggExpr.name.toLowerCase()) { case `sum`: diff --git a/packages/db/src/query/compiler/grouping.ts b/packages/db/src/query/compiler/grouping.ts new file mode 100644 index 0000000000..1e4280ed56 --- /dev/null +++ b/packages/db/src/query/compiler/grouping.ts @@ -0,0 +1,9 @@ +import { processGroupBy, replaceAggregatesByRefs } from './group-by.js' + +/** + * The grouping compiler. Aggregate values and `groupKeys` carry it onto the + * query IR, so apps that never group or aggregate do not bundle it. + */ +export const grouping = { processGroupBy, replaceAggregatesByRefs } + +export type Grouping = typeof grouping diff --git a/packages/db/src/query/compiler/index.ts b/packages/db/src/query/compiler/index.ts index a19835d712..b102efcc8c 100644 --- a/packages/db/src/query/compiler/index.ts +++ b/packages/db/src/query/compiler/index.ts @@ -23,6 +23,7 @@ import { CollectionInputNotFoundError, DistinctRequiresSelectError, FnSelectWithGroupByError, + GroupingOperatorRequiredError, HavingRequiresGroupByError, LimitOffsetRequireOrderByError, NonAggregateExpressionNotInGroupByError, @@ -67,7 +68,8 @@ import { toBooleanPredicate, } from './evaluators.js' import { registerLazyDemandPlan } from './lazy-demand.js' -import { containsAggregate, processGroupBy } from './group-by.js' +import { containsAggregate } from './group-by.js' +import { findAggregate } from './find-aggregate.js' import { getLazyLoadTargets } from './lazy-targets.js' import { processOrderBy } from './order-by.js' import { crossJoinParentRoutes } from './parent-routes.js' @@ -1108,8 +1110,17 @@ function compileQueryInternal( unionRows && ref.bindingId === undefined && ref.path[0] !== `$selected` ) } + // An aggregate value or groupKeys brings the grouping compiler. + const grouping = + query.grouping ?? + findAggregate([ + query.select, + query.having, + query.orderBy?.map((clause) => clause.expression), + ])?.grouping if (query.groupBy && query.groupBy.length > 0) { - pipeline = processGroupBy( + if (!grouping) throw new GroupingOperatorRequiredError() + pipeline = grouping.processGroupBy( pipeline, query.groupBy, valueIdentity, @@ -1123,7 +1134,7 @@ function compileQueryInternal( ) } else if (selectHasAggregates) { // SELECT contains aggregates but no GROUP BY: implicit single-group aggregation - pipeline = processGroupBy( + pipeline = grouping!.processGroupBy( pipeline, [], // Empty group by means single group valueIdentity, diff --git a/packages/db/src/query/compiler/order-by.ts b/packages/db/src/query/compiler/order-by.ts index 96ac88ed12..6511ff129c 100644 --- a/packages/db/src/query/compiler/order-by.ts +++ b/packages/db/src/query/compiler/order-by.ts @@ -20,7 +20,7 @@ import { hasVirtualPropPath } from '../../virtual-props.js' import { devBuild } from '../../error-message.js' import { compileExpression } from './evaluators.js' import { getSourceAliasesFromExpression } from './expressions.js' -import { replaceAggregatesByRefs } from './group-by.js' +import { findAggregate } from './find-aggregate.js' import type { CompareOptions } from '../builder/types.js' import type { WindowOptions } from './types.js' import type { CompiledSingleRowExpression } from './evaluators.js' @@ -132,12 +132,16 @@ export function processOrderBy( groupKeyFn?: (key: unknown, value: unknown) => unknown, ): IStreamBuilder> { const compiledOrderBy = orderByClause.map((clause) => { - // Aggregate references read the SELECT result under $selected. - const clauseWithoutAggregates = replaceAggregatesByRefs( - clause.expression, - selectClause, - `$selected`, - ) + // Aggregate references read the SELECT result under $selected. The + // aggregate brings the code that rewrites them. + const aggregate = findAggregate(clause.expression) + const clauseWithoutAggregates = aggregate + ? aggregate.grouping.replaceAggregatesByRefs( + clause.expression, + selectClause, + `$selected`, + ) + : clause.expression return { compiledExpression: compileExpression(clauseWithoutAggregates), diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index 5d486bd9e9..719127fd65 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -35,6 +35,9 @@ export { type Prettify, } from './builder/index.js' +// Grouping without aggregates +export { groupKeys, type GroupKeys } from './builder/group-keys.js' + // Join operators export { leftJoin, diff --git a/packages/db/src/query/ir.ts b/packages/db/src/query/ir.ts index e2d25a227d..7e0db52d44 100644 --- a/packages/db/src/query/ir.ts +++ b/packages/db/src/query/ir.ts @@ -11,6 +11,7 @@ import type { CollectionOptionsIdentity } from '../collection-options.js' import type { NamespacedRow } from '../types' import type { processJoins } from './compiler/joins.js' +import type { Grouping } from './compiler/grouping.js' import type { applyOptimizations } from './optimizer-joins.js' export interface QueryIR { @@ -19,6 +20,8 @@ export interface QueryIR { join?: Join where?: Array groupBy?: GroupBy + // groupKeys supplies the grouping compiler when no aggregate does. + grouping?: Grouping having?: Array orderBy?: OrderBy limit?: Limit @@ -269,6 +272,8 @@ export class Aggregate extends BaseExpression { constructor( public name: string, // such as count, avg, sum, min, max, etc. public args: Array, + // The aggregate function brings the grouping compiler with it. + public grouping: Grouping, ) { super() } diff --git a/packages/db/tests/error-sample-arguments.ts b/packages/db/tests/error-sample-arguments.ts index b72fb20bc9..eca05f00cc 100644 --- a/packages/db/tests/error-sample-arguments.ts +++ b/packages/db/tests/error-sample-arguments.ts @@ -92,6 +92,7 @@ export const errorSampleArguments: Record>> = { ['unionAll clause', 'undefined'], ], JoinOperatorRequiredError: [['object']], + GroupingOperatorRequiredError: [[]], JoinConditionMustBeEqualityError: [[]], QueryMustHaveFromClauseError: [[]], InvalidWhereExpressionError: [['boolean']], diff --git a/packages/db/tests/fixtures/error-codes.json b/packages/db/tests/fixtures/error-codes.json index 27197cea5d..cd66de43e9 100644 --- a/packages/db/tests/fixtures/error-codes.json +++ b/packages/db/tests/fixtures/error-codes.json @@ -91,5 +91,6 @@ "GetKeyRequiredError": 178, "DuplicateTransactionIdError": 233, "SetWindowFindOneLimitError": 244, - "JoinOperatorRequiredError": 245 + "JoinOperatorRequiredError": 245, + "GroupingOperatorRequiredError": 246 } diff --git a/packages/db/tests/fixtures/error-messages.json b/packages/db/tests/fixtures/error-messages.json index 81fd2f071c..f4b04f0e3b 100644 --- a/packages/db/tests/fixtures/error-messages.json +++ b/packages/db/tests/fixtures/error-messages.json @@ -263,5 +263,8 @@ ], "JoinOperatorRequiredError": [ ".join() takes a join operator, such as .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Import leftJoin, innerJoin, rightJoin, fullJoin, or joinOf from @tanstack/db. Got: object" + ], + "GroupingOperatorRequiredError": [ + "groupBy() without an aggregate needs groupKeys(), such as .groupBy(groupKeys(({ todo }) => todo.status)). Import groupKeys from @tanstack/db. A query with an aggregate such as count() needs nothing extra." ] } diff --git a/packages/db/tests/query/aggregate-select-oracle.property.test.ts b/packages/db/tests/query/aggregate-select-oracle.property.test.ts index 780c768a68..3dbd641e60 100644 --- a/packages/db/tests/query/aggregate-select-oracle.property.test.ts +++ b/packages/db/tests/query/aggregate-select-oracle.property.test.ts @@ -1,19 +1,18 @@ import { describe, expect, it } from 'vitest' import { fc } from '@fast-check/vitest' -import { createCollection } from '../../src/collection/index.js' -import { - add, +import { add, caseWhen, coalesce, count, createLiveQueryCollection, eq, + groupKeys, inArray, max, sum, toArray, - upper, -} from '../../src/query/index.js' + upper } from '../../src/query/index.js' +import { createCollection } from '../../src/collection/index.js' import { AggregateFunctionNotInSelectError, NonAggregateExpressionNotInGroupByError, @@ -614,7 +613,9 @@ async function runHistory( alias === undefined ? refs : refs[alias] const group = (query: any, alias: string | undefined) => grouped - ? query.groupBy((refs: any) => keyExpression(rowOf(refs, alias))) + ? query.groupBy( + groupKeys((refs: any) => keyExpression(rowOf(refs, alias))), + ) : query const having = ( s: Shape, @@ -905,7 +906,7 @@ describe(`aggregate select shapes`, () => { const query = createLiveQueryCollection((q) => q .from({ c: source }) - .groupBy(({ c }) => key(c)) + .groupBy(groupKeys(({ c }) => key(c))) .select(({ c }) => ({ g: key(c), n: count(c.id) })), ) await query.preload() diff --git a/packages/db/tests/query/aggregate-value-contracts.test-d.ts b/packages/db/tests/query/aggregate-value-contracts.test-d.ts index fa303970c0..4114706306 100644 --- a/packages/db/tests/query/aggregate-value-contracts.test-d.ts +++ b/packages/db/tests/query/aggregate-value-contracts.test-d.ts @@ -1,5 +1,5 @@ import { describe, expectTypeOf, test } from 'vitest' -import { createLiveQueryCollection, leftJoin } from '../../src/query/index.js' +import { createLiveQueryCollection, leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { add, @@ -244,8 +244,11 @@ describe(`aggregate value contracts`, () => { query: (q) => q .from({ row: rows }) - .join(leftJoin({ detail: details }, ({ row, detail }) => - eq(row.id, detail.rowId))) + .join( + leftJoin({ detail: details }, ({ row, detail }) => + eq(row.id, detail.rowId), + ), + ) .groupBy(({ row }) => row.group) .select(({ row, detail }) => { expectTypeOf(sum(detail.amount)).toEqualTypeOf>() diff --git a/packages/db/tests/query/async-hold-outcomes.test.ts b/packages/db/tests/query/async-hold-outcomes.test.ts index 11ea94cf21..38e16a10f2 100644 --- a/packages/db/tests/query/async-hold-outcomes.test.ts +++ b/packages/db/tests/query/async-hold-outcomes.test.ts @@ -1,5 +1,9 @@ import { afterEach, describe, expect, it } from 'vitest' -import { createLiveQueryCollection, eq, leftJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + leftJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { createEffect } from '../../src/query/effect.js' @@ -160,8 +164,11 @@ describe(`live-query lazy demand hold outcomes`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ user: users }) - .join(leftJoin({ post: posts.collection }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: posts.collection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ id: user.id, postId: post.id })), ) const subscription = liveQuery.subscribeChanges(() => {}) @@ -189,8 +196,11 @@ describe(`live-query lazy demand hold outcomes`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ user: users }) - .join(leftJoin({ post: posts.collection }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: posts.collection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ id: user.id, postId: post.id })), ) cleanups.push(() => liveQuery.cleanup()) diff --git a/packages/db/tests/query/builder/ref-proxy.test.ts b/packages/db/tests/query/builder/ref-proxy.test.ts index b3e1af381f..531ca254a1 100644 --- a/packages/db/tests/query/builder/ref-proxy.test.ts +++ b/packages/db/tests/query/builder/ref-proxy.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { grouping } from '../../../src/query/compiler/grouping.js' import { createRefProxy, createRefProxyWithSelected, @@ -257,7 +258,7 @@ describe(`ref-proxy`, () => { it(`preserves constructed functions and aggregates`, () => { const funcExpr = new Func(`upper`, []) - const aggExpr = new Aggregate(`count`, []) + const aggExpr = new Aggregate(`count`, [], grouping) expect(toExpression(funcExpr)).toBe(funcExpr) expect(toExpression(aggExpr)).toBe(aggExpr) diff --git a/packages/db/tests/query/case-when.test-d.ts b/packages/db/tests/query/case-when.test-d.ts index 3444ade271..1dc9a98dc1 100644 --- a/packages/db/tests/query/case-when.test-d.ts +++ b/packages/db/tests/query/case-when.test-d.ts @@ -1,10 +1,12 @@ import { describe, expectTypeOf, test } from 'vitest' -import { caseWhen, +import { + caseWhen, createLiveQueryCollection, eq, gt, leftJoin, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -289,8 +291,11 @@ describe(`caseWhen types`, () => { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join(leftJoin({ post: posts }, - ({ user, post }) => eq(user.id, post.userId))) + .join( + leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ id: user.id, postStatus: caseWhen(post, `has-post`, `no-post`), diff --git a/packages/db/tests/query/case-when.test.ts b/packages/db/tests/query/case-when.test.ts index d4ff592833..8a155ffde3 100644 --- a/packages/db/tests/query/case-when.test.ts +++ b/packages/db/tests/query/case-when.test.ts @@ -1,11 +1,14 @@ import { describe, expect, test } from 'vitest' -import { caseWhen, count, +import { + caseWhen, + count, createLiveQueryCollection, eq, gt, innerJoin, leftJoin, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' @@ -436,8 +439,11 @@ describe(`caseWhen`, () => { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join(leftJoin({ post: posts }, - ({ user, post }) => eq(user.id, post.userId))) + .join( + leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ id: user.id, title: caseWhen(post, post.title, `none`), @@ -464,9 +470,11 @@ describe(`caseWhen`, () => { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join(innerJoin({ post: posts }, - ({ user, post }) => - eq(caseWhen(eq(user.active, true), user.id, -1), post.userId))) + .join( + innerJoin({ post: posts }, ({ user, post }) => + eq(caseWhen(eq(user.active, true), user.id, -1), post.userId), + ), + ) .select(({ user, post }) => ({ userId: user.id, title: post.title, diff --git a/packages/db/tests/query/clone-query.test.ts b/packages/db/tests/query/clone-query.test.ts index b4caf8a88d..65d5a4d32a 100644 --- a/packages/db/tests/query/clone-query.test.ts +++ b/packages/db/tests/query/clone-query.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { grouping } from '../../src/query/compiler/grouping.js' import { cloneQueryForPlacement } from '../../src/query/builder/clone-query.js' import { Aggregate, @@ -14,7 +15,7 @@ describe(`cloneQueryForPlacement`, () => { it.each([ new PropRef([`u`, `name`]), new Func(`upper`, [new PropRef([`u`, `name`])]), - new Aggregate(`count`, [new PropRef([`u`, `id`])]), + new Aggregate(`count`, [new PropRef([`u`, `id`])], grouping), new ConditionalSelect( [ { diff --git a/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts b/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts index f71b8bb888..7d8cf471a0 100644 --- a/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts +++ b/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts @@ -1,13 +1,18 @@ import fc from 'fast-check' import { Temporal } from 'temporal-polyfill' import { describe, expect, it } from 'vitest' -import { Query, and, createLiveQueryCollection, eq , +import { + Query, + and, + createLiveQueryCollection, + eq, fullJoin as fullJoinOperator, gt, innerJoin, joinOf, leftJoin, - or } from '../../src/query/index.js' + or, +} from '../../src/query/index.js' import { Func } from '../../src/query/ir.js' import { JoinConditionMustBeEqualityError } from '../../src/errors.js' import { createCollection } from '../../src/collection/index.js' @@ -145,8 +150,11 @@ async function runColdJoin( const live = createLiveQueryCollection((q) => q .from({ parent: parentSource }) - .join(innerJoin({ child: children }, ({ parent, child }) => - eq(parent.id, child.parentId))) + .join( + innerJoin({ child: children }, ({ parent, child }) => + eq(parent.id, child.parentId), + ), + ) .select(({ parent, child }) => ({ id: child.id, parentId: parent.id, @@ -504,8 +512,11 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left }) - .join(innerJoin({ right }, ({ left: l, right: r }) => - eq(l.value, r.value))) + .join( + innerJoin({ right }, ({ left: l, right: r }) => + eq(l.value, r.value), + ), + ) .select(({ right: row }) => ({ id: row.id })), }) const filtered = createLiveQueryCollection({ @@ -567,8 +578,11 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left: binaryLeft }) - .join(innerJoin({ right: binaryRight }, ({ left, right }) => - eq(left.value, right.value))) + .join( + innerJoin({ right: binaryRight }, ({ left, right }) => + eq(left.value, right.value), + ), + ) .select(({ right }) => ({ id: right.id })), }) @@ -594,8 +608,11 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left: nullishLeft }) - .join(fullJoinOperator({ right: nullishRight }, ({ left, right }) => - eq(left.value, right.value))) + .join( + fullJoinOperator({ right: nullishRight }, ({ left, right }) => + eq(left.value, right.value), + ), + ) .select(({ left, right }) => ({ leftId: left.id, rightId: right.id, @@ -654,8 +671,11 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left }) - .join(fullJoinOperator({ right }, ({ left: l, right: r }) => - eq(l.value, r.value))) + .join( + fullJoinOperator({ right }, ({ left: l, right: r }) => + eq(l.value, r.value), + ), + ) .select(({ left: l, right: r }) => ({ leftId: l.id, rightId: r.id, @@ -792,8 +812,11 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left: active }) - .join(leftJoin({ right: lazy }, ({ left, right }) => - eq(left.binaryId, right.binaryId))) + .join( + leftJoin({ right: lazy }, ({ left, right }) => + eq(left.binaryId, right.binaryId), + ), + ) .select(({ left, right }) => ({ leftId: left.id, rightId: right.id, @@ -1116,8 +1139,8 @@ async function runCompoundHistory( const live = createLiveQueryCollection((q) => q .from({ left }) - .join(joinOf(scenario.join, { right }, - ({ left: l, right: r }) => { + .join( + joinOf(scenario.join, { right }, ({ left: l, right: r }) => { const a = scenario.reversed ? eq(r.a, l.a) : eq(l.a, r.a) const b = scenario.reversed ? eq(l.b, r.b) : eq(r.b, l.b) const c = eq(l.c, r.c) @@ -1125,7 +1148,8 @@ async function runCompoundHistory( if (scenario.width === 2) return and(first, second) if (scenario.nested) return and(first, and(second, c)) return and(first, second, c) - })) + }), + ) .select(({ left: l, right: r }) => ({ leftId: l.id, rightId: r.id })), ) cleanupLive = () => live.cleanup() @@ -1396,15 +1420,15 @@ it.each([`or`, `inequality`, `empty`, `nested-invalid`] as const)( await checkWithCleanup( () => { expect(() => - new Query() - .from({ left }) - .join(innerJoin({ right }, ({ left: l, right: r }) => { + new Query().from({ left }).join( + innerJoin({ right }, ({ left: l, right: r }) => { const equality = eq(l.id, r.id) if (shape === `or`) return or(equality, equality) if (shape === `inequality`) return gt(l.id, r.id) if (shape === `empty`) return new Func(`and`, []) return and(equality, and(equality, gt(l.id, r.id))) - })), + }), + ), ).toThrow(JoinConditionMustBeEqualityError) return Promise.resolve() }, diff --git a/packages/db/tests/query/collection-ref-compatibility.test.ts b/packages/db/tests/query/collection-ref-compatibility.test.ts index 62fbf03a03..51513bf4b5 100644 --- a/packages/db/tests/query/collection-ref-compatibility.test.ts +++ b/packages/db/tests/query/collection-ref-compatibility.test.ts @@ -106,7 +106,9 @@ describe(`CollectionRef public shape`, () => { query: { from: ref(`root`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: ref(`joined`), type: `inner`, on: new IR.Func(`eq`, [ diff --git a/packages/db/tests/query/compiler/group-by-pipeline.test.ts b/packages/db/tests/query/compiler/group-by-pipeline.test.ts index ee4794774e..ddd13fb250 100644 --- a/packages/db/tests/query/compiler/group-by-pipeline.test.ts +++ b/packages/db/tests/query/compiler/group-by-pipeline.test.ts @@ -1,5 +1,6 @@ import { D2, MultiSet, output } from '@tanstack/db-ivm' import { describe, expect, test } from 'vitest' +import { grouping } from '../../../src/query/compiler/grouping.js' import { NonAggregateExpressionNotInGroupByError } from '../../../src/errors.js' import { coalesce } from '../../../src/query/builder/functions.js' import { processGroupBy } from '../../../src/query/compiler/group-by.js' @@ -133,7 +134,7 @@ describe(`group-by production pipeline`, () => { const graph = new D2() const input = graph.newInput() const groupRef = new PropRef([`row`, `group`]) - const total = new Aggregate(`sum`, [new PropRef([`row`, `amount`])]) + const total = new Aggregate(`sum`, [new PropRef([`row`, `amount`])], grouping) // Exercise generated-field collision avoidance as well as wrapped refs. const totalAlias = `__tanstack_group_synced` const select: Select | undefined = diff --git a/packages/db/tests/query/compiler/group-by.test.ts b/packages/db/tests/query/compiler/group-by.test.ts index 0ec27fd54f..ad6cecf116 100644 --- a/packages/db/tests/query/compiler/group-by.test.ts +++ b/packages/db/tests/query/compiler/group-by.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { grouping } from '../../../src/query/compiler/grouping.js' import { Aggregate, Func, PropRef, Value } from '../../../src/query/ir.js' // Import the validation function that we want to test directly @@ -73,8 +74,8 @@ describe(`group-by compiler`, () => { const groupByClause = [new PropRef([`users`, `department`])] const selectClause = { department: new PropRef([`users`, `department`]), - count: new Aggregate(`count`, [new PropRef([`users`, `id`])]), - avg_salary: new Aggregate(`avg`, [new PropRef([`users`, `salary`])]), + count: new Aggregate(`count`, [new PropRef([`users`, `id`])], grouping), + avg_salary: new Aggregate(`avg`, [new PropRef([`users`, `salary`])], grouping), } // Should not throw diff --git a/packages/db/tests/query/compiler/select.test.ts b/packages/db/tests/query/compiler/select.test.ts index c5459944ae..2a25a34e59 100644 --- a/packages/db/tests/query/compiler/select.test.ts +++ b/packages/db/tests/query/compiler/select.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { grouping } from '../../../src/query/compiler/grouping.js' import { compileExpression } from '../../../src/query/compiler/evaluators.js' import { Aggregate, Func, PropRef, Value } from '../../../src/query/ir.js' @@ -33,7 +34,7 @@ describe(`select compiler`, () => { }) it(`throws error for aggregate expressions`, () => { - const arg = new Aggregate(`count`, [new PropRef([`users`, `id`])]) + const arg = new Aggregate(`count`, [new PropRef([`users`, `id`])], grouping) const namespacedRow = { users: { id: 1 } } expect(() => { @@ -161,11 +162,11 @@ describe(`select compiler`, () => { // Test the helper function that can be imported and tested directly it(`rejects aggregate IR at the single-row compiler boundary`, () => { const aggregateExpressions = [ - new Aggregate(`count`, [new PropRef([`users`, `id`])]), - new Aggregate(`sum`, [new PropRef([`orders`, `amount`])]), - new Aggregate(`avg`, [new PropRef([`products`, `price`])]), - new Aggregate(`min`, [new PropRef([`dates`, `created`])]), - new Aggregate(`max`, [new PropRef([`dates`, `updated`])]), + new Aggregate(`count`, [new PropRef([`users`, `id`])], grouping), + new Aggregate(`sum`, [new PropRef([`orders`, `amount`])], grouping), + new Aggregate(`avg`, [new PropRef([`products`, `price`])], grouping), + new Aggregate(`min`, [new PropRef([`dates`, `created`])], grouping), + new Aggregate(`max`, [new PropRef([`dates`, `updated`])], grouping), ] const namespacedRow = { diff --git a/packages/db/tests/query/composables.test.ts b/packages/db/tests/query/composables.test.ts index cadec16d7b..9bc70ad30d 100644 --- a/packages/db/tests/query/composables.test.ts +++ b/packages/db/tests/query/composables.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, test } from 'vitest' -import { Query, +import { + Query, and, createLiveQueryCollection, eq, @@ -8,7 +9,8 @@ import { Query, lower, lt, lte, - upper } from '../../src/query/index.js' + upper, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { Ref } from '../../src/query/index.js' @@ -268,8 +270,11 @@ describe(`Composables`, () => { query: (q) => q .from({ user: usersCollection }) - .join(innerJoin({ post: postsCollection }, - ({ user, post }) => eq(user.id, post.authorId))) + .join( + innerJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.authorId), + ), + ) .where(userIsActive) .where(postIsPublished) .select(userPostJoinInfo), @@ -419,8 +424,11 @@ describe(`Composables`, () => { query: (q) => q .from({ u: usersCollection }) - .join(innerJoin({ p: postsCollection }, - ({ u, p }) => eq(u.id, p.authorId))) + .join( + innerJoin({ p: postsCollection }, ({ u, p }) => + eq(u.id, p.authorId), + ), + ) .where(joinedUserPostPredicate) .select(joinedSelect), startSync: true, diff --git a/packages/db/tests/query/distinct.test.ts b/packages/db/tests/query/distinct.test.ts index 68f3748707..15c87ce2b4 100644 --- a/packages/db/tests/query/distinct.test.ts +++ b/packages/db/tests/query/distinct.test.ts @@ -1,5 +1,9 @@ import { beforeEach, describe, expect, test } from 'vitest' -import { concat, createLiveQueryCollection, leftJoin } from '../../src/query/index.js' +import { + concat, + createLiveQueryCollection, + leftJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import { DistinctRequiresSelectError } from '../../src/errors' @@ -696,9 +700,13 @@ function createDistinctTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ users: usersCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ users, departments }) => - eq(users.department, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ users, departments }) => + eq(users.department, departments.id), + ), + ) .where(({ users }) => eq(users.active, true)) .select(({ departments }) => ({ department: departments.id, diff --git a/packages/db/tests/query/findone-joins.test-d.ts b/packages/db/tests/query/findone-joins.test-d.ts index e45a7fff6f..ef455efbe5 100644 --- a/packages/db/tests/query/findone-joins.test-d.ts +++ b/packages/db/tests/query/findone-joins.test-d.ts @@ -1,5 +1,12 @@ import { describe, expectTypeOf, test } from 'vitest' -import { createLiveQueryCollection, eq, fullJoin, innerJoin, leftJoin, rightJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + fullJoin, + innerJoin, + leftJoin, + rightJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -45,8 +52,12 @@ describe(`findOne() with joins`, () => { .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) .orderBy(({ todo }) => todo.order, `asc`) - .join(leftJoin({ todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) + .join( + leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), + ), + ) .findOne(), }) @@ -66,8 +77,12 @@ describe(`findOne() with joins`, () => { q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) - .join(innerJoin({ todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) + .join( + innerJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), + ), + ) .findOne(), }) @@ -88,8 +103,12 @@ describe(`findOne() with joins`, () => { .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) .findOne() - .join(leftJoin({ todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))), + .join( + leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), + ), + ), }) expectTypeOf(query.toArray).toMatchTypeOf< @@ -108,8 +127,12 @@ describe(`findOne() with joins`, () => { q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) - .join(rightJoin({ todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) + .join( + rightJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), + ), + ) .findOne(), }) @@ -129,8 +152,12 @@ describe(`findOne() with joins`, () => { q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) - .join(fullJoin({ todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) + .join( + fullJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), + ), + ) .findOne(), }) @@ -163,10 +190,17 @@ describe(`findOne() with joins`, () => { query: (q) => q .from({ todo: todoCollection }) - .join(leftJoin({ todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) - .join(innerJoin({ tag: todoTagsCollection }, ({ todo, tag }) => - eq(todo.id, tag.todoId))) + .join( + leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), + ), + ) + .join( + innerJoin({ tag: todoTagsCollection }, ({ todo, tag }) => + eq(todo.id, tag.todoId), + ), + ) .findOne(), }) @@ -186,8 +220,12 @@ describe(`findOne() with joins`, () => { query: (q) => q .from({ todo: todoCollection }) - .join(leftJoin({ todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) + .join( + leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), + ), + ) .select(({ todo, todoOptions }) => ({ todoText: todo.text, optionText: todoOptions.optionText, @@ -210,8 +248,12 @@ describe(`findOne() with joins`, () => { query: (q) => q .from({ todo: todoCollection }) - .join(leftJoin({ todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) + .join( + leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), + ), + ) .findOne() .select(({ todo, todoOptions }) => ({ todoText: todo.text, @@ -236,8 +278,12 @@ describe(`findOne() with joins`, () => { .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) .orderBy(({ todo }) => todo.order, `asc`) - .join(leftJoin({ todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId))) + .join( + leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), + ), + ) .limit(1), }) diff --git a/packages/db/tests/query/functional-variants.test-d.ts b/packages/db/tests/query/functional-variants.test-d.ts index 5b168970d4..777b2aa77d 100644 --- a/packages/db/tests/query/functional-variants.test-d.ts +++ b/packages/db/tests/query/functional-variants.test-d.ts @@ -1,5 +1,6 @@ import { describe, expectTypeOf, test } from 'vitest' -import { Query, +import { + Query, caseWhen, count, createLiveQueryCollection, @@ -7,7 +8,8 @@ import { Query, gt, leftJoin, materialize, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -270,8 +272,11 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .fn.select((row) => ({ employeeInfo: `${row.user.name} works in ${row.dept?.name || `Unknown`}`, isHighEarner: row.user.salary > 70000, @@ -304,8 +309,11 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .fn.where( (row) => row.user.active && (row.dept?.name === `Engineering` || false), @@ -329,8 +337,11 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .fn.where((row) => row.user.active) .fn.where((row) => row.user.salary > 60000) .fn.select((row) => ({ @@ -527,8 +538,11 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .groupBy(({ dept }) => dept.name) .fn.having((row) => row.dept?.name !== `HR`) .select(({ dept, user }) => ({ diff --git a/packages/db/tests/query/functional-variants.test.ts b/packages/db/tests/query/functional-variants.test.ts index 8f0655acb7..3673ad3951 100644 --- a/packages/db/tests/query/functional-variants.test.ts +++ b/packages/db/tests/query/functional-variants.test.ts @@ -1,12 +1,14 @@ import { beforeEach, describe, expect, test } from 'vitest' -import { caseWhen, +import { + caseWhen, count, createLiveQueryCollection, eq, gt, leftJoin, materialize, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' @@ -191,8 +193,11 @@ describe(`Functional Variants Query`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .fn.select((row) => ({ employeeInfo: `${row.user.name} works in ${row.dept?.name || `Unknown`}`, isHighEarner: row.user.salary > 70000, @@ -848,8 +853,11 @@ describe(`Functional Variants Query`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .fn.where((row) => row.user.active) .fn.where((row) => row.user.salary > 60000) .fn.select((row) => ({ diff --git a/packages/db/tests/query/generic-collection.test-d.ts b/packages/db/tests/query/generic-collection.test-d.ts index b50a04b6f2..07ef63c117 100644 --- a/packages/db/tests/query/generic-collection.test-d.ts +++ b/packages/db/tests/query/generic-collection.test-d.ts @@ -1,5 +1,9 @@ import { describe, test } from 'vitest' -import { createLiveQueryCollection, eq, leftJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + leftJoin, +} from '../../src/query/index.js' import type { Collection } from '../../src/collection/index.js' // Regression tests for https://github.com/TanStack/db/issues/1677 @@ -42,8 +46,11 @@ describe(`queries over generic collection row types`, () => { const sub = q.from({ a }) return q .from({ b }) - .join(leftJoin({ sub }, ({ b: bRef, sub: subRef }) => - eq(bRef.id, subRef.id))) + .join( + leftJoin({ sub }, ({ b: bRef, sub: subRef }) => + eq(bRef.id, subRef.id), + ), + ) }) } diff --git a/packages/db/tests/query/group-by.test.ts b/packages/db/tests/query/group-by.test.ts index 13d29c49bb..9f8522c99d 100644 --- a/packages/db/tests/query/group-by.test.ts +++ b/packages/db/tests/query/group-by.test.ts @@ -8,8 +8,16 @@ */ import { beforeEach, describe, expect, test } from 'vitest' import { Temporal } from 'temporal-polyfill' -import { createLiveQueryCollection, leftJoin, toArray } from '../../src/query/index.js' -import { NonAggregateExpressionNotInGroupByError } from '../../src/errors.js' +import { + createLiveQueryCollection, + groupKeys, + leftJoin, + toArray, +} from '../../src/query/index.js' +import { + GroupingOperatorRequiredError, + NonAggregateExpressionNotInGroupByError, +} from '../../src/errors.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' import { @@ -2415,8 +2423,11 @@ function createGroupByTests(autoIndex: `off` | `eager`): void { return q .from({ customer: customersCollection }) - .join(leftJoin({ oc: orderCountSubquery }, ({ customer, oc }) => - eq(customer.id, oc.customer_id))) + .join( + leftJoin({ oc: orderCountSubquery }, ({ customer, oc }) => + eq(customer.id, oc.customer_id), + ), + ) .select(({ customer, oc }) => ({ name: customer.name, orderCount: oc.orderCount, @@ -2716,7 +2727,7 @@ describe(`group value among equal instances`, () => { const grouped = createLiveQueryCollection((q) => q .from({ row: source }) - .groupBy(({ row }) => row.v) + .groupBy(groupKeys(({ row }) => row.v)) .select(({ row }) => ({ v: row.v })), ) const write = (type: `insert` | `delete`, id: number) => { @@ -2892,3 +2903,62 @@ describe(`non-aggregate fields of an aggregate without groupBy`, () => { ).toThrow(NonAggregateExpressionNotInGroupByError) }) }) + +describe(`grouping without an aggregate`, () => { + type Row = { id: number; v: string } + const rows = () => + createCollection( + mockSyncCollectionOptions({ + id: `grouping-operator`, + getKey: (row) => row.id, + initialData: [ + { id: 1, v: `a` }, + { id: 2, v: `a` }, + { id: 3, v: `b` }, + ], + }), + ) + + test(`needs groupKeys and names it`, () => { + const source = rows() + expect(() => + createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ row: source }) + .groupBy(({ row }) => row.v) + .select(({ row }) => ({ v: row.v })), + }), + ).toThrow(GroupingOperatorRequiredError) + }) + + test(`groups by keys with groupKeys, and an aggregate needs nothing extra`, async () => { + const source = rows() + const keysOnly = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ row: source }) + .groupBy(groupKeys(({ row }) => row.v)) + .select(({ row }) => ({ v: row.v })), + }) + const counted = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ row: source }) + .groupBy(({ row }) => row.v) + .select(({ row }) => ({ v: row.v, n: count(row.id) })), + }) + await Promise.all([keysOnly.preload(), counted.preload()]) + expect(keysOnly.toArray.map(stripVirtualProps)).toEqual([ + { v: `a` }, + { v: `b` }, + ]) + expect(counted.toArray.map(stripVirtualProps)).toEqual([ + { v: `a`, n: 2 }, + { v: `b`, n: 1 }, + ]) + }) +}) diff --git a/packages/db/tests/query/identity-output-shape-oracle.test.ts b/packages/db/tests/query/identity-output-shape-oracle.test.ts index 428117697a..d83d2e863f 100644 --- a/packages/db/tests/query/identity-output-shape-oracle.test.ts +++ b/packages/db/tests/query/identity-output-shape-oracle.test.ts @@ -3,11 +3,13 @@ import { D2, MultiSet, output, serializeValue } from '@tanstack/db-ivm' import { Temporal } from 'temporal-polyfill' import fc from 'fast-check' import { describe, expect, it } from 'vitest' -import { Query, +import { + Query, and, createLiveQueryCollection, eq, - innerJoin } from '../../src/query/index.js' + innerJoin, +} from '../../src/query/index.js' import { processJoins } from '../../src/query/compiler/joins.js' import { applyOptimizations } from '../../src/query/optimizer-joins.js' import { getQueryIR } from '../../src/query/builder/query-ir.js' @@ -156,7 +158,8 @@ async function runShapes( from: user, join: [ { - compile: processJoins, optimize: applyOptimizations, + compile: processJoins, + optimize: applyOptimizations, type: 'inner', from: post, on: new Func('eq', [ @@ -669,14 +672,16 @@ describe(`compound join identity and output`, () => { ) => { const joined = new Query() .from({ left }) - .join(innerJoin({ right }, ({ left: l, right: r }) => { - const a = eq(l.a, r.a) - const b = eq(l.b, variant === `different` ? r.c : r.b) - if (variant === `reordered`) - return and(eq(r.b, l.b), eq(r.a, l.a)) - if (variant === `nested`) return and(b, and(a, b)) - return and(a, b) - })) + .join( + innerJoin({ right }, ({ left: l, right: r }) => { + const a = eq(l.a, r.a) + const b = eq(l.b, variant === `different` ? r.c : r.b) + if (variant === `reordered`) + return and(eq(r.b, l.b), eq(r.a, l.a)) + if (variant === `nested`) return and(b, and(a, b)) + return and(a, b) + }), + ) .select(({ left: l, right: r }) => ({ leftId: l.id, rightId: r.id, @@ -700,8 +705,11 @@ describe(`compound join identity and output`, () => { } const query = new Query() .from({ anchor: left }) - .join(innerJoin({ result: joined }, ({ anchor, result }) => - eq(anchor.id, result.leftId))) + .join( + innerJoin({ result: joined }, ({ anchor, result }) => + eq(anchor.id, result.leftId), + ), + ) .select(({ result }) => ({ leftId: result.leftId, rightId: result.rightId, diff --git a/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts b/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts index 47dfe65fcb..16bf78a9ae 100644 --- a/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts +++ b/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from 'vitest' -import { Query, add, caseWhen , +import { + Query, + add, + caseWhen, count, createLiveQueryCollection, eq, @@ -9,7 +12,8 @@ import { Query, add, caseWhen , joinOf, leftJoin, multiply, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { getQueryIR } from '../../src/query/builder/query-ir.js' import { IncludesSubquery, PropRef } from '../../src/query/ir.js' import { getQueryIdentity } from '../../src/query/ir-stable-identity.js' @@ -235,15 +239,21 @@ function buildQuery( return toArray( new Query() .unionAll(left, right) - .join(innerJoin({ anchor: locks.collection }, ({ lockId, anchor }) => - eq(lockId, anchor.id))) + .join( + innerJoin({ anchor: locks.collection }, ({ lockId, anchor }) => + eq(lockId, anchor.id), + ), + ) .where(({ anchor }) => eq(anchor.id, parent.id)) .select(({ id, lockId }) => ({ id, lockId })), ) } return toArray( - base().join(innerJoin({ other: locks.collection }, (context: Context) => - eq(voteAt(context, alias).lockId, (context.other as Lock).id))), + base().join( + innerJoin({ other: locks.collection }, (context: Context) => + eq(voteAt(context, alias).lockId, (context.other as Lock).id), + ), + ), ) })() return { id: parent.id, children } @@ -459,8 +469,11 @@ describe(`captured alias scope oracle`, () => { return createLiveQueryCollection({ query: new Query() .from({ manager: managers.collection }) - .join(innerJoin({ helper: shared }, ({ manager, helper }) => - eq(manager.id, helper.id))) + .join( + innerJoin({ helper: shared }, ({ manager, helper }) => + eq(manager.id, helper.id), + ), + ) .select(({ manager }) => reportsFor(manager as unknown as Employee), ), @@ -619,8 +632,11 @@ describe(`captured alias scope oracle`, () => { const live = createLiveQueryCollection({ query: new Query() .unionAll(first, second) - .join(innerJoin({ anchor: source.collection }, ({ id, anchor }) => - eq(id, anchor.id))) + .join( + innerJoin({ anchor: source.collection }, ({ id, anchor }) => + eq(id, anchor.id), + ), + ) .select(({ anchor }) => ({ id: anchor.id, children: toArray( @@ -660,8 +676,11 @@ describe(`captured alias scope oracle`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ wrapped: inner }) - .join(innerJoin({ anchor: source.collection }, ({ wrapped, anchor }) => - eq(wrapped.id, anchor.id))) + .join( + innerJoin({ anchor: source.collection }, ({ wrapped, anchor }) => + eq(wrapped.id, anchor.id), + ), + ) .select(({ anchor }) => ({ id: anchor.id, children: toArray( @@ -797,8 +816,12 @@ describe(`captured alias scope oracle`, () => { children: toArray( new Query() .from({ lock: votes.collection }) - .join(innerJoin({ other: locks.collection }, - ({ lock: child, other }) => eq(child.lockId, other.id))) + .join( + innerJoin( + { other: locks.collection }, + ({ lock: child, other }) => eq(child.lockId, other.id), + ), + ) .where(({ lock: child, other }) => eq(child.lockId, other.id)) .where(({ lock: child }) => eq(child.lockId, parent.id)) .select(({ lock: child }) => ({ @@ -1282,8 +1305,11 @@ describe(`outer filters stay with their source under alias shadowing`, () => { form === `join` ? createLiveQueryCollection({ query: root - .join(leftJoin({ w: childQuery }, ({ u, w }) => - eq(u.linkId, w.id))) + .join( + leftJoin({ w: childQuery }, ({ u, w }) => + eq(u.linkId, w.id), + ), + ) .select(({ u, w }) => ({ id: u.id, joinedId: w.id })), }) : createLiveQueryCollection({ @@ -1428,8 +1454,11 @@ describe(`captured projections keep their binding during predicate pushdown`, () matches: toArray( new Query() .from({ q: inner }) - .join(innerJoin({ a: anchors.collection }, ({ q, a }) => - eq(q.id, a.id))) + .join( + innerJoin({ a: anchors.collection }, ({ q, a }) => + eq(q.id, a.id), + ), + ) .where(({ q }) => eq(q.parentId, parent.id)) .where(({ q }) => eq(q.rank, `A`)) .select(({ q }) => ({ id: q.id })), @@ -1695,8 +1724,11 @@ describe(`group keys through nested source filtering`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ x: inner }) - .join(innerJoin({ t: tags.collection }, ({ x, t }) => - eq(x.id, t.sourceId))) + .join( + innerJoin({ t: tags.collection }, ({ x, t }) => + eq(x.id, t.sourceId), + ), + ) .where(({ x }) => eq(x.category, `A`)) .groupBy(({ x }) => x.category) .select(({ x }) => ({ @@ -2100,8 +2132,11 @@ describe(`lexical bindings through recursive joins and selections`, () => { return createLiveQueryCollection({ query: new Query() .from({ manager: managers }) - .join(leftJoin({ user: people.collection }, ({ manager, user }) => - eq(manager.id, user.managerId))) + .join( + leftJoin({ user: people.collection }, ({ manager, user }) => + eq(manager.id, user.managerId), + ), + ) .where(({ user }) => eq(user.managerId, 1)) .select(({ manager, user }) => ({ managerId: manager.id, @@ -2172,8 +2207,11 @@ describe(`lexical bindings through recursive joins and selections`, () => { return createLiveQueryCollection({ query: new Query() .from({ manager: inner }) - .join(leftJoin({ user: users.collection }, ({ manager, user }) => - eq(manager.id, user.managerId))) + .join( + leftJoin({ user: users.collection }, ({ manager, user }) => + eq(manager.id, user.managerId), + ), + ) .select(({ manager, user }) => ({ managerId: manager.id, userId: user.id, @@ -2289,11 +2327,14 @@ describe(`lexical bindings through recursive joins and selections`, () => { children: toArray( new Query() .from({ [childAlias]: children.collection }) - .join(innerJoin({ anchor: anchorQuery }, (context: Context) => - eq( - (context[childAlias] as { anchorId: number }).anchorId, - (context.anchor as { id: number }).id, - ))) + .join( + innerJoin({ anchor: anchorQuery }, (context: Context) => + eq( + (context[childAlias] as { anchorId: number }).anchorId, + (context.anchor as { id: number }).id, + ), + ), + ) .where((context: Context) => eq( (context[childAlias] as { parentId: number }).parentId, @@ -2363,12 +2404,16 @@ describe(`lexical bindings through recursive joins and selections`, () => { children: toArray( new Query() .from({ comment: comments.collection }) - .join(leftJoin({ [joinAlias]: anchors.collection }, - (context: Context) => - eq( - (context.comment as { anchorId: number }).anchorId, - (context[joinAlias] as { id: number }).id, - ))) + .join( + leftJoin( + { [joinAlias]: anchors.collection }, + (context: Context) => + eq( + (context.comment as { anchorId: number }).anchorId, + (context[joinAlias] as { id: number }).id, + ), + ), + ) .where((context: Context) => eq( (context.comment as { parentId: number }).parentId, @@ -2839,8 +2884,12 @@ describe(`nested source placement preserves captured outer rows`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ manager: managers.collection }) - .join(innerJoin({ user: assignments.collection }, - ({ manager, user }) => eq(manager.id, user.managerId))) + .join( + innerJoin( + { user: assignments.collection }, + ({ manager, user }) => eq(manager.id, user.managerId), + ), + ) .select(({ manager, user }) => { const project = (context: Context) => { const detail = context[alias] as Detail @@ -2865,12 +2914,16 @@ describe(`nested source placement preserves captured outer rows`, () => { .select(project) : new Query() .from({ anchor: anchors.collection }) - .join(innerJoin({ [alias]: details.collection }, - (context: Context) => - eq( - (context[alias] as Detail).managerId, - (context.anchor as { id: number }).id, - ))) + .join( + innerJoin( + { [alias]: details.collection }, + (context: Context) => + eq( + (context[alias] as Detail).managerId, + (context.anchor as { id: number }).id, + ), + ), + ) .where((context: Context) => eq( (context.anchor as { id: number }).id, @@ -3164,7 +3217,9 @@ describe(`outer joins preserve absent source bindings under shadowing`, () => { ) const anchorSource = sourceForm === `direct` ? anchors.collection : scopedAnchors - const joined = main.join(joinOf(kind, { anchor: anchorSource }, on)) + const joined = main.join( + joinOf(kind, { anchor: anchorSource }, on), + ) expect(getQueryIR(joined).join?.[0]?.from.type).toBe( sourceForm === `direct` ? `collectionRef` : `queryRef`, ) @@ -3426,25 +3481,32 @@ describe(`chained joins keep an absent local source absent`, () => { rows: toArray( new Query() .from({ comment: comments.collection }) - .join(leftJoin({ [alias]: localIssues.collection }, - (context: Context) => - eq( - (context.comment as Comment).localIssueId, - (context[alias] as Issue).id, - ))) - .join(leftJoin({ tag: tagSource }, (context: Context) => - routeCause === `queryRef` - ? eq( - (context.comment as Comment).tagId, - (context.tag as Tag).id, - ) - : eq( - add( + .join( + leftJoin( + { [alias]: localIssues.collection }, + (context: Context) => + eq( + (context.comment as Comment).localIssueId, + (context[alias] as Issue).id, + ), + ), + ) + .join( + leftJoin({ tag: tagSource }, (context: Context) => + routeCause === `queryRef` + ? eq( (context.comment as Comment).tagId, - parent.id, + (context.tag as Tag).id, + ) + : eq( + add( + (context.comment as Comment).tagId, + parent.id, + ), + add((context.tag as Tag).id, parent.id), ), - add((context.tag as Tag).id, parent.id), - ))) + ), + ) .where((context: Context) => eq((context.comment as Comment).issueId, parent.id), ) @@ -3625,8 +3687,11 @@ describe(`joined QueryRef lazy targets retain lexical source identity`, () => { }) const joined = new Query() .from({ [alias]: posts.collection }) - .join(innerJoin({ tag: tags.collection }, (context: Context) => - eq((context[alias] as Post).tagId, (context.tag as Tag).id))) + .join( + innerJoin({ tag: tags.collection }, (context: Context) => + eq((context[alias] as Post).tagId, (context.tag as Tag).id), + ), + ) .select((context: Context) => ({ tagId: (context.tag as Tag).id, title: (context[alias] as Post).title, @@ -3889,18 +3954,28 @@ describe(`mixed outer-join chains retain absent source roles`, () => { rows: toArray( new Query() .from({ [mainAlias]: mains.collection }) - .join(joinOf(first, { [anchorAlias]: anchors.collection }, - (context: Context) => - eq( - (context[mainAlias] as Main).anchorId, - (context[anchorAlias] as Anchor).id, - ))) - .join(joinOf(second, { tag: tags.collection }, - (context: Context) => - eq( - (context[anchorAlias] as Anchor).id, - (context.tag as Tag).anchorId, - ))) + .join( + joinOf( + first, + { [anchorAlias]: anchors.collection }, + (context: Context) => + eq( + (context[mainAlias] as Main).anchorId, + (context[anchorAlias] as Anchor).id, + ), + ), + ) + .join( + joinOf( + second, + { tag: tags.collection }, + (context: Context) => + eq( + (context[anchorAlias] as Anchor).id, + (context.tag as Tag).anchorId, + ), + ), + ) .where((context: Context) => correlation === `main` ? eq((context[mainAlias] as Main).parentId, parent.id) @@ -4094,24 +4169,28 @@ describe(`captured ancestor join operands retain their bindings`, () => { rows: toArray( new Query() .from({ child: children.collection }) - .join(innerJoin({ [alias]: targets.collection }, - (context: Context) => { - const child = context.child as Child - const target = context[alias] as Target - const mainKey = - ancestorSide === `main` - ? add(child.base, ancestor.offset) - : ancestorSide === `soleMain` - ? ancestor.offset - : child.base - const joinedKey = - ancestorSide === `joined` - ? add(target.id, ancestor.offset) - : target.id - return order === `childFirst` - ? eq(mainKey, joinedKey) - : eq(joinedKey, mainKey) - })) + .join( + innerJoin( + { [alias]: targets.collection }, + (context: Context) => { + const child = context.child as Child + const target = context[alias] as Target + const mainKey = + ancestorSide === `main` + ? add(child.base, ancestor.offset) + : ancestorSide === `soleMain` + ? ancestor.offset + : child.base + const joinedKey = + ancestorSide === `joined` + ? add(target.id, ancestor.offset) + : target.id + return order === `childFirst` + ? eq(mainKey, joinedKey) + : eq(joinedKey, mainKey) + }, + ), + ) .where((context: Context) => eq((context.child as Child).parentId, ancestor.id), ) @@ -4270,8 +4349,11 @@ describe(`join operands reject captured sources outside lexical scope`, () => { const child = (operand: number) => new Query() .from({ local: local.collection }) - .join(innerJoin({ joined: joined.collection }, ({ joined: target }) => - eq(operand, target.id))) + .join( + innerJoin({ joined: joined.collection }, ({ joined: target }) => + eq(operand, target.id), + ), + ) .select(({ local: own, joined: target }) => ({ localId: own.id, joinedId: target.id, @@ -4303,8 +4385,12 @@ describe(`join operands reject captured sources outside lexical scope`, () => { const localControl = createLiveQueryCollection({ query: new Query() .from({ local: local.collection }) - .join(innerJoin({ joined: joined.collection }, - ({ local: own, joined: target }) => eq(own.id, target.id))) + .join( + innerJoin( + { joined: joined.collection }, + ({ local: own, joined: target }) => eq(own.id, target.id), + ), + ) .select(({ local: own, joined: target }) => ({ localId: own.id, joinedId: target.id, @@ -4898,8 +4984,12 @@ describe(`include joins reject sources hidden inside parent operands`, () => { rows: toArray( new Query() .from({ local: local.collection }) - .join(innerJoin({ joined: joined.collection }, - ({ joined: target }) => eq(hiddenRef, target.id))) + .join( + innerJoin( + { joined: joined.collection }, + ({ joined: target }) => eq(hiddenRef, target.id), + ), + ) .where(({ local: row }) => eq(row.id, parentId)) .select(({ local: row, joined: target }) => ({ localId: row.id, @@ -4986,14 +5076,20 @@ describe(`joined subqueries reject captured sibling sources`, () => { }) const joinedQuery = new Query() .from({ [alias]: inner.collection }) - .join(innerJoin({ probe: probe.collection }, ({ probe: row }) => - eq(siblingRef, row.id))) + .join( + innerJoin({ probe: probe.collection }, ({ probe: row }) => + eq(siblingRef, row.id), + ), + ) .select((context: Context) => ({ id: (context[alias] as Row).id, })) const child = base - .join(innerJoin({ nested: joinedQuery }, ({ local: row, nested }) => - eq(row.id, nested.id))) + .join( + innerJoin({ nested: joinedQuery }, ({ local: row, nested }) => + eq(row.id, nested.id), + ), + ) .select(({ local: row, nested }) => ({ localId: row.id, joinedId: nested.id, @@ -5087,8 +5183,11 @@ describe(`joined subqueries reject captured sibling sources`, () => { .where((context: Context) => eq((context[alias] as Row).id, ancestor.id), ) - .join(innerJoin({ probe: probe.collection }, ({ probe: row }) => - eq(ancestor.id, row.id))) + .join( + innerJoin({ probe: probe.collection }, ({ probe: row }) => + eq(ancestor.id, row.id), + ), + ) .select((context: Context) => ({ id: (context[alias] as Row).id, ancestorId: ancestor.id, @@ -5098,8 +5197,12 @@ describe(`joined subqueries reject captured sibling sources`, () => { rows: toArray( new Query() .from({ local: main.collection }) - .join(innerJoin({ nested: joinedQuery }, - ({ local: row, nested }) => eq(row.id, nested.id))) + .join( + innerJoin( + { nested: joinedQuery }, + ({ local: row, nested }) => eq(row.id, nested.id), + ), + ) .where(({ local: row }) => eq(row.id, ancestor.id)) .select(({ local: row, nested }) => ({ localId: row.id, @@ -5165,8 +5268,11 @@ test(`mixed bound and unbound same-alias join references name separate sources`, rows: toArray( new Query() .from({ child: child.collection }) - .join(innerJoin({ issue: local.collection }, () => - eq(ancestor.id, new PropRef([`issue`, `id`])))) + .join( + innerJoin({ issue: local.collection }, () => + eq(ancestor.id, new PropRef([`issue`, `id`])), + ), + ) .where(({ child: row }) => eq(row.parentId, ancestor.id)) .select(({ child: row, issue }) => ({ childId: row.id, diff --git a/packages/db/tests/query/includes-collection-oracle.property.test.ts b/packages/db/tests/query/includes-collection-oracle.property.test.ts index f107eff592..885dfb913e 100644 --- a/packages/db/tests/query/includes-collection-oracle.property.test.ts +++ b/packages/db/tests/query/includes-collection-oracle.property.test.ts @@ -1,6 +1,8 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect } from 'vitest' -import { add, caseWhen, +import { + add, + caseWhen, concat, count, createLiveQueryCollection, @@ -12,7 +14,8 @@ import { add, caseWhen, materialize, multiply, sum, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { runTrace } from '../trace-runner.js' @@ -1662,8 +1665,11 @@ describe(`Collection-valued includes oracle`, () => { query: (q) => { const joined = q .from({ comment: comments.collection }) - .join(leftJoin({ user: users.collection }, ({ comment, user }) => - eq(comment.userId, user.id))) + .join( + leftJoin({ user: users.collection }, ({ comment, user }) => + eq(comment.userId, user.id), + ), + ) return mode === `value` ? joined.select(({ comment }) => ({ publicId: comment.userId, @@ -3107,8 +3113,11 @@ describe(`Collection-valued includes oracle`, () => { const childRows = () => q .from({ child: children.collection }) - .join(innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(tag.id, add(child.tagId, parent.offset)))) + .join( + innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(tag.id, add(child.tagId, parent.offset)), + ), + ) .where(({ child }) => eq(child.parentGroup, parent.group)) .select(({ child, tag }) => ({ id: child.id, diff --git a/packages/db/tests/query/includes-context-transport-oracle.test.ts b/packages/db/tests/query/includes-context-transport-oracle.test.ts index 4d5134929d..d5b75d5bfc 100644 --- a/packages/db/tests/query/includes-context-transport-oracle.test.ts +++ b/packages/db/tests/query/includes-context-transport-oracle.test.ts @@ -6,6 +6,7 @@ import { add, count, createLiveQueryCollection, eq, + groupKeys, gt, innerJoin, lt, @@ -157,8 +158,11 @@ test('keeps dotted ancestor aliases distinct in nested includes', async () => { const live = createLiveQueryCollection((q) => q .from({ 'p.q': dotted.collection }) - .join(innerJoin({ p: nested.collection }, ({ 'p.q': left, p }) => - eq(left.id, p.id))) + .join( + innerJoin({ p: nested.collection }, ({ 'p.q': left, p }) => + eq(left.id, p.id), + ), + ) .select(({ 'p.q': left, p }) => ({ id: left.id, children: toArray( @@ -916,7 +920,7 @@ async function runAggregationCell({ const rows = grouping === `explicit` ? correlated - .groupBy(({ child }) => child.parentGroup) + .groupBy(groupKeys(({ child }) => child.parentGroup)) .select(({ child }) => ({ score: placement === `inside-aggregate` @@ -1045,10 +1049,12 @@ async function runRecursiveSourceCell({ })) case `aggregate`: return correlated - .groupBy(({ candidate }) => [ - candidate.id, - candidate.parentGroup, - ]) + .groupBy( + groupKeys(({ candidate }) => [ + candidate.id, + candidate.parentGroup, + ]), + ) .select(({ candidate }) => ({ id: candidate.id, parentGroup: candidate.parentGroup, @@ -1056,10 +1062,12 @@ async function runRecursiveSourceCell({ })) case `having`: return correlated - .groupBy(({ candidate }) => [ - candidate.id, - candidate.parentGroup, - ]) + .groupBy( + groupKeys(({ candidate }) => [ + candidate.id, + candidate.parentGroup, + ]), + ) .having(({ candidate }) => gt(count(candidate.id), parent.parameter), ) @@ -1111,10 +1119,12 @@ async function runRecursiveSourceCell({ })) case `aggregate`: return correlated - .groupBy(({ leftCandidate }) => [ - leftCandidate.id, - leftCandidate.parentGroup, - ]) + .groupBy( + groupKeys(({ leftCandidate }) => [ + leftCandidate.id, + leftCandidate.parentGroup, + ]), + ) .select(({ leftCandidate }) => ({ id: leftCandidate.id, parentGroup: leftCandidate.parentGroup, @@ -1122,10 +1132,12 @@ async function runRecursiveSourceCell({ })) case `having`: return correlated - .groupBy(({ leftCandidate }) => [ - leftCandidate.id, - leftCandidate.parentGroup, - ]) + .groupBy( + groupKeys(({ leftCandidate }) => [ + leftCandidate.id, + leftCandidate.parentGroup, + ]), + ) .having(({ leftCandidate }) => gt(count(leftCandidate.id), parent.parameter), ) @@ -1174,10 +1186,12 @@ async function runRecursiveSourceCell({ })) case `aggregate`: return correlated - .groupBy(({ rightCandidate }) => [ - rightCandidate.id, - rightCandidate.parentGroup, - ]) + .groupBy( + groupKeys(({ rightCandidate }) => [ + rightCandidate.id, + rightCandidate.parentGroup, + ]), + ) .select(({ rightCandidate }) => ({ id: rightCandidate.id, parentGroup: rightCandidate.parentGroup, @@ -1185,10 +1199,12 @@ async function runRecursiveSourceCell({ })) case `having`: return correlated - .groupBy(({ rightCandidate }) => [ - rightCandidate.id, - rightCandidate.parentGroup, - ]) + .groupBy( + groupKeys(({ rightCandidate }) => [ + rightCandidate.id, + rightCandidate.parentGroup, + ]), + ) .having(({ rightCandidate }) => gt(count(rightCandidate.id), parent.parameter), ) @@ -1226,8 +1242,12 @@ async function runRecursiveSourceCell({ case `joined-query-ref`: { const routed = q .from({ anchor: anchors.collection }) - .join(innerJoin({ result: buildCandidates(candidates.collection) }, - ({ anchor, result }) => eq(anchor.id, result.id))) + .join( + innerJoin( + { result: buildCandidates(candidates.collection) }, + ({ anchor, result }) => eq(anchor.id, result.id), + ), + ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ result }) => ({ id: result.id, @@ -1238,8 +1258,11 @@ async function runRecursiveSourceCell({ case `union-branch`: { const routed = q .unionAll(buildLeftCandidates(), buildRightCandidates()) - .join(innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => - eq(id, anchor.id))) + .join( + innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => + eq(id, anchor.id), + ), + ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ id, value }) => ({ id, value })) return { id: parent.id, ...includeInEveryForm(routed) } @@ -1383,8 +1406,11 @@ async function runUnionIdentityCell({ })) return q .unionAll(leftRows, rightRows) - .join(innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => - eq(id, anchor.id))) + .join( + innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => + eq(id, anchor.id), + ), + ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ id, value }) => ({ id, value })) } @@ -1394,9 +1420,13 @@ async function runUnionIdentityCell({ leftCandidate: left.collection, rightCandidate: right.collection, }) - .join(innerJoin({ anchor: anchors.collection }, - ({ leftCandidate, rightCandidate, anchor }) => - eq(coalesce(leftCandidate.id, rightCandidate.id), anchor.id))) + .join( + innerJoin( + { anchor: anchors.collection }, + ({ leftCandidate, rightCandidate, anchor }) => + eq(coalesce(leftCandidate.id, rightCandidate.id), anchor.id), + ), + ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ leftCandidate, rightCandidate }) => ({ id: coalesce(leftCandidate.id, rightCandidate.id), @@ -1522,8 +1552,11 @@ async function runDerivedResultCell({ ) const rows = q .from({ anchor: anchors.collection }) - .join(innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value))) + .join( + innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value), + ), + ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1536,8 +1569,11 @@ async function runDerivedResultCell({ if (boundary === `from-query-ref`) { const rows = q .from({ value: values }) - .join(innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => - eq(value, anchor.value))) + .join( + innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => + eq(value, anchor.value), + ), + ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1545,8 +1581,11 @@ async function runDerivedResultCell({ const rows = q .from({ anchor: anchors.collection }) - .join(innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value))) + .join( + innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value), + ), + ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1558,8 +1597,11 @@ async function runDerivedResultCell({ if (boundary === `from-query-ref`) { const rows = q .from({ value: values }) - .join(innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => - eq(value, anchor.value))) + .join( + innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => + eq(value, anchor.value), + ), + ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1567,8 +1609,11 @@ async function runDerivedResultCell({ const rows = q .from({ anchor: anchors.collection }) - .join(innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value))) + .join( + innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value), + ), + ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1688,9 +1733,13 @@ async function runQueryRefMetadataCell( })) return q .from({ anchor: anchors.collection }) - .join(innerJoin({ candidateResult: projectedCandidates }, - ({ anchor, candidateResult }) => - eq(anchor.candidateId, candidateResult.id))) + .join( + innerJoin( + { candidateResult: projectedCandidates }, + ({ anchor, candidateResult }) => + eq(anchor.candidateId, candidateResult.id), + ), + ) .select(({ candidateResult }) => candidateResult) }) @@ -1721,9 +1770,13 @@ async function runQueryRefMetadataCell( })) const rows = q .from({ anchor: anchors.collection }) - .join(innerJoin({ candidateResult: projectedCandidates }, - ({ anchor, candidateResult }) => - eq(anchor.candidateId, candidateResult.id))) + .join( + innerJoin( + { candidateResult: projectedCandidates }, + ({ anchor, candidateResult }) => + eq(anchor.candidateId, candidateResult.id), + ), + ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ candidateResult }) => candidateResult) return { id: parent.id, ...includeInEveryForm(rows) } @@ -1781,12 +1834,18 @@ async function runJoinCell({ keySide === `main` ? q .from({ child: children.collection }) - .join(innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(tag.id, add(child.tagId, parent.offset)))) + .join( + innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(tag.id, add(child.tagId, parent.offset)), + ), + ) : q .from({ child: children.collection }) - .join(innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(add(tag.id, parent.offset), child.tagId))) + .join( + innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(add(tag.id, parent.offset), child.tagId), + ), + ) const routed = ( correlationAttachment === `main` ? joined.where(({ child }) => eq(child.parentGroup, parent.group)) @@ -1904,8 +1963,11 @@ async function runNamespaceCollisionCell({ case `join`: return includeInEveryForm( correlated - .join(innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(child.id, tag.id))) + .join( + innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(child.id, tag.id), + ), + ) .select(({ child }) => ({ id: child.id, value: child.label, @@ -1914,7 +1976,9 @@ async function runNamespaceCollisionCell({ case `group`: return includeInEveryForm( correlated - .groupBy(({ child }) => [child.id, child.label]) + .groupBy( + groupKeys(({ child }) => [child.id, child.label]), + ) .select(({ child }) => ({ id: child.id, value: child.label, @@ -1965,8 +2029,11 @@ async function runNamespaceCollisionCell({ case `join`: return includeInEveryForm( correlated - .join(innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(child.id, tag.id))) + .join( + innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(child.id, tag.id), + ), + ) .select(({ child }) => ({ id: child.id, [name]: child.label, @@ -1975,7 +2042,9 @@ async function runNamespaceCollisionCell({ case `group`: return includeInEveryForm( correlated - .groupBy(({ child }) => [child.id, child.label]) + .groupBy( + groupKeys(({ child }) => [child.id, child.label]), + ) .select(({ child }) => ({ id: child.id, [name]: child.label, @@ -2137,8 +2206,11 @@ async function runPublicSurfaceCell({ .fn.select(({ candidate }) => candidate.value) const rows = q .from({ anchor: anchors.collection }) - .join(innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value))) + .join( + innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value), + ), + ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) return { id: parent.id, ...includeInEveryForm(rows) } @@ -2170,7 +2242,7 @@ async function runPublicSurfaceCell({ if (shape === `functional-having-input`) { const rows = correlated - .groupBy(({ child }) => child.parentGroup) + .groupBy(groupKeys(({ child }) => child.parentGroup)) .select(({ child }) => ({ parentGroup: child.parentGroup, total: count(child.id), @@ -2207,8 +2279,11 @@ async function runPublicSurfaceCell({ return { id: parent.id, ...includeInEveryForm(rows) } } - const rows = correlated.join(innerJoin({ tag: tags.collection }, - ({ child, tag }) => eq(child.id, tag.childId))) + const rows = correlated.join( + innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(child.id, tag.childId), + ), + ) return { id: parent.id, ...includeInEveryForm(rows) } }), ) @@ -2664,17 +2739,23 @@ describe(`compound join parent context`, () => { ) const joined = boundary === `direct` - ? base.join(innerJoin({ peer: peers.collection }, joinCondition)) - : base.join(innerJoin({ - peer: q - .from({ peer: peers.collection }) - .select(({ peer }) => ({ - id: peer.id, - a: peer.a, - b: peer.b, - })), - }, - joinCondition)) + ? base.join( + innerJoin({ peer: peers.collection }, joinCondition), + ) + : base.join( + innerJoin( + { + peer: q + .from({ peer: peers.collection }) + .select(({ peer }) => ({ + id: peer.id, + a: peer.a, + b: peer.b, + })), + }, + joinCondition, + ), + ) const rows = joined.select(({ child, peer }) => ({ id: child.id, peerId: peer.id, diff --git a/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts b/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts index 80ecf5ce77..327d96662c 100644 --- a/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts +++ b/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts @@ -1,7 +1,8 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, test } from 'vitest' import { Temporal } from 'temporal-polyfill' -import { and, +import { + and, count, createLiveQueryCollection, eq, @@ -10,7 +11,8 @@ import { and, lt, not, queryOnce, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { Func, PropRef, Value } from '../../src/query/ir.js' import { createCollection } from '../../src/collection/index.js' import { createFilterFunctionFromExpression } from '../../src/collection/change-events.js' @@ -540,8 +542,11 @@ function createFlatQuery( query: (q) => q .from({ parent: parents }) - .join(leftJoin({ child: children }, ({ parent, child }) => - eq(parent.group, child.parentGroup))) + .join( + leftJoin({ child: children }, ({ parent, child }) => + eq(parent.group, child.parentGroup), + ), + ) .select(({ parent, child }) => ({ parentId: parent.id, parentGroup: parent.group, diff --git a/packages/db/tests/query/includes-lazy-loading.test.ts b/packages/db/tests/query/includes-lazy-loading.test.ts index 08716f32fa..3c2476acc0 100644 --- a/packages/db/tests/query/includes-lazy-loading.test.ts +++ b/packages/db/tests/query/includes-lazy-loading.test.ts @@ -1,10 +1,12 @@ import { describe, expect, it, vi } from 'vitest' -import { and, +import { + and, createLiveQueryCollection, eq, gte, leftJoin, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { extractSimpleComparisons } from '../../src/query/expression-helpers.js' import { @@ -190,8 +192,11 @@ describe(`includes lazy loading`, () => { matches: toArray( q .from({ left: items }) - .join(leftJoin({ right: items }, ({ left, right }) => - eq(left.peerId, right.id))) + .join( + leftJoin({ right: items }, ({ left, right }) => + eq(left.peerId, right.id), + ), + ) .where(({ right }) => eq(right.rootId, root.id)) .select(({ left, right }) => ({ left: left.label, diff --git a/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts b/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts index 4350dbc705..c19a267c9c 100644 --- a/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts +++ b/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts @@ -1,9 +1,11 @@ import { describe, expect, test, vi } from 'vitest' -import { Query, +import { + Query, createLiveQueryCollection, eq, joinOf, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { BasicIndex } from '../../src/indexes/basic-index.js' import { withHistoryCleanup } from '../optimistic-history-oracle.js' import { flushPromises } from '../utils.js' @@ -152,8 +154,13 @@ describe(`direct outer-join route work`, () => { rows: toArray( new Query() .from({ main: mains.collection }) - .join(joinOf(kind, { anchor: anchors.collection }, - ({ main, anchor }) => eq(main.anchorId, anchor.id))) + .join( + joinOf( + kind, + { anchor: anchors.collection }, + ({ main, anchor }) => eq(main.anchorId, anchor.id), + ), + ) .where(({ main }) => eq(main.parentId, parent.id)) .select(({ main, anchor }) => ({ mainId: main.id, diff --git a/packages/db/tests/query/includes-publication-oracle.test.ts b/packages/db/tests/query/includes-publication-oracle.test.ts index 74d19fd501..e78293a834 100644 --- a/packages/db/tests/query/includes-publication-oracle.test.ts +++ b/packages/db/tests/query/includes-publication-oracle.test.ts @@ -1,10 +1,12 @@ import { setImmediate as yieldToRunner } from 'node:timers/promises' import { fc, test as fcTest } from '@fast-check/vitest' import { afterEach, describe, expect, it } from 'vitest' -import { createLiveQueryCollection, +import { + createLiveQueryCollection, eq, innerJoin, - materialize } from '../../src/query/index.js' + materialize, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BasicIndex } from '../../src/indexes/basic-index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' @@ -133,9 +135,13 @@ function createLayeredQuery( const parentRows = q1Shape === `direct` ? source - : source.join(innerJoin({ metadata: metadata.collection }, - ({ item, metadata: rowMetadata }) => - eq(item.id, rowMetadata.parentId))) + : source.join( + innerJoin( + { metadata: metadata.collection }, + ({ item, metadata: rowMetadata }) => + eq(item.id, rowMetadata.parentId), + ), + ) return parentRows.select(({ item }) => ({ item, diff --git a/packages/db/tests/query/includes-query-shape-oracle.test.ts b/packages/db/tests/query/includes-query-shape-oracle.test.ts index 7c2e1d9627..746e78cd97 100644 --- a/packages/db/tests/query/includes-query-shape-oracle.test.ts +++ b/packages/db/tests/query/includes-query-shape-oracle.test.ts @@ -1,9 +1,11 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect } from 'vitest' -import { createLiveQueryCollection, +import { + createLiveQueryCollection, eq, innerJoin, - materialize } from '../../src/query/index.js' + materialize, +} from '../../src/query/index.js' import { BasicIndex } from '../../src/indexes/basic-index.js' import { runTrace } from '../trace-runner.js' import { oraclePropertyOptions, oracleRuns } from '../oracle-config.js' @@ -124,8 +126,12 @@ function createMultiplicityQuery(sources: MultiplicitySources) { query: (q) => q .from({ parent: sources.parents.collection }) - .join(innerJoin({ child: sources.children.collection }, - ({ parent, child }) => eq(parent.id, child.parentId))) + .join( + innerJoin( + { child: sources.children.collection }, + ({ parent, child }) => eq(parent.id, child.parentId), + ), + ) .select(({ parent }) => ({ id: parent.id })), getKey: (row) => row.id, }) @@ -228,8 +234,12 @@ function createCorrelationQuery( .select(({ part }) => { const joined = q .from({ production: sources.productions.collection }) - .join(innerJoin({ order: sources.orders.collection }, - ({ production, order }) => eq(production.orderId, order.id))) + .join( + innerJoin( + { order: sources.orders.collection }, + ({ production, order }) => eq(production.orderId, order.id), + ), + ) const correlated = target === `joined` ? joined.where(({ order }) => eq(order.partId, part.id)) diff --git a/packages/db/tests/query/includes-scope-identity-oracle.ts b/packages/db/tests/query/includes-scope-identity-oracle.ts index 1fb8e8318d..fae90bbfff 100644 --- a/packages/db/tests/query/includes-scope-identity-oracle.ts +++ b/packages/db/tests/query/includes-scope-identity-oracle.ts @@ -1,11 +1,15 @@ import { fc } from '@fast-check/vitest' -import { and, createLiveQueryCollection, eq , +import { + and, + createLiveQueryCollection, + eq, innerJoin, joinOf, leftJoin, materialize, not, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createFilterFunctionFromExpression } from '../../src/collection/change-events.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -613,8 +617,11 @@ export function createScopedQuery( branch(n.activeBranch!, true), branch(n.inactiveBranch!, false), ) - .join(innerJoin({ [n.anchor!]: sources.parts.collection }, (c: Context) => - eq(c.id, c[n.anchor!].id))) + .join( + innerJoin({ [n.anchor!]: sources.parts.collection }, (c: Context) => + eq(c.id, c[n.anchor!].id), + ), + ) .select((c: Context) => ({ id: c.id, active: c.active, @@ -645,8 +652,11 @@ export function createScopedQuery( [n.wrapped!]: q.from({ [n.wrapped!]: sources.parts.collection }), } as any) as any ) - .join(joinOf(shape.wrapperJoin, { [n.joinSub!]: clientOne }, - (c: Context) => eq(c[n.joinSub!].partId, c[n.wrapped!].id))) + .join( + joinOf(shape.wrapperJoin, { [n.joinSub!]: clientOne }, (c: Context) => + eq(c[n.joinSub!].partId, c[n.wrapped!].id), + ), + ) .select((c: Context) => ({ id: c[n.wrapped!].id, active: c[n.wrapped!].active, @@ -657,12 +667,20 @@ export function createScopedQuery( const { subquery, include } = shape let sub: any = q.from({ [n.sub!]: sources.parts.collection }) if (subquery.joins !== `none`) { - sub = sub.join(joinOf(subquery.refsJoin, { [n.joined!]: sources.refs.collection }, - (c: Context) => eq(c[n.joined!].partId, c[n.sub!].id))) + sub = sub.join( + joinOf( + subquery.refsJoin, + { [n.joined!]: sources.refs.collection }, + (c: Context) => eq(c[n.joined!].partId, c[n.sub!].id), + ), + ) } if (subquery.joins === `refsThenNotes`) { - sub = sub.join(leftJoin({ [n.noted!]: sources.notes.collection }, - (c: Context) => eq(c[n.noted!].partId, c[n.sub!].id))) + sub = sub.join( + leftJoin({ [n.noted!]: sources.notes.collection }, (c: Context) => + eq(c[n.noted!].partId, c[n.sub!].id), + ), + ) } const predicates = subquery.predicates.map((predicate) => predicate === `subActive` @@ -718,8 +736,12 @@ export function createScopedQuery( branch(n.include!, clientOne), branch(n.includeOther!, (member) => not(clientOne(member))), ) - .join(innerJoin({ [n.includeAnchor!]: includeCollection }, - (cc: Context) => eq(cc.memberId, cc[n.includeAnchor!].id))) + .join( + innerJoin( + { [n.includeAnchor!]: includeCollection }, + (cc: Context) => eq(cc.memberId, cc[n.includeAnchor!].id), + ), + ) .where((cc: Context) => eq(cc[n.includeAnchor!].partId, outer.id)) if (include.clientFilter) { child = child.where((cc: Context) => @@ -748,8 +770,12 @@ export function createScopedQuery( }) : q.from({ [member]: includeCollection }) if (include.body === `joined`) { - child = child.join(innerJoin({ [n.includeJoin!]: sources.parts.collection }, - (cc: Context) => eq(cc[n.includeJoin!].id, cc[member].partId))) + child = child.join( + innerJoin( + { [n.includeJoin!]: sources.parts.collection }, + (cc: Context) => eq(cc[n.includeJoin!].id, cc[member].partId), + ), + ) } child = child.where((cc: Context) => eq(cc[member].partId, outer.id)) if (include.clientFilter) { diff --git a/packages/db/tests/query/includes-temporal-oracle.test.ts b/packages/db/tests/query/includes-temporal-oracle.test.ts index 2d877d163c..af7ed12ab9 100644 --- a/packages/db/tests/query/includes-temporal-oracle.test.ts +++ b/packages/db/tests/query/includes-temporal-oracle.test.ts @@ -1,11 +1,13 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, it, vi } from 'vitest' -import { createLiveQueryCollection, +import { + createLiveQueryCollection, eq, leftJoin, materialize, queryOnce, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { BasicIndex } from '../../src/indexes/basic-index.js' @@ -2984,7 +2986,9 @@ function laggingSourceQuery( })) return q .from({ user: users! }) - .join(leftJoin({ post: posts }, ({ user, post }) => eq(user.id, post.userId))) + .join( + leftJoin({ post: posts }, ({ user, post }) => eq(user.id, post.userId)), + ) .select(({ user, post }) => ({ id: user.id, postId: post.id })) } } diff --git a/packages/db/tests/query/includes-work-counter-oracle.test.ts b/packages/db/tests/query/includes-work-counter-oracle.test.ts index 16a33fb9c2..a8a60fb958 100644 --- a/packages/db/tests/query/includes-work-counter-oracle.test.ts +++ b/packages/db/tests/query/includes-work-counter-oracle.test.ts @@ -1,11 +1,14 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { beforeAll, describe, expect, it } from 'vitest' -import { createLiveQueryCollection, eq, +import { + createLiveQueryCollection, + eq, inArray, innerJoin, leftJoin, materialize, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { oraclePropertyOptions, oracleRuns } from '../oracle-config.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' @@ -321,9 +324,13 @@ async function observeWork({ links: joinTargets ? materialize( selectedLinks - .join(innerJoin({ target: sources.terms }, - ({ link, target }) => - eq(link.targetId, target.id))) + .join( + innerJoin( + { target: sources.terms }, + ({ link, target }) => + eq(link.targetId, target.id), + ), + ) .select(({ link, target }) => ({ id: link.id, text: target.text, @@ -2077,7 +2084,11 @@ describe(`static include demand and setWindow ownership`, () => { ? q.from({ page }) : q .from({ row: rows }) - .join(leftJoin({ page }, ({ row, page: paged }) => eq(row.id, paged.id))) + .join( + leftJoin({ page }, ({ row, page: paged }) => + eq(row.id, paged.id), + ), + ) }) try { await live.preload() diff --git a/packages/db/tests/query/includes.test.ts b/packages/db/tests/query/includes.test.ts index 81bdc135fa..323abe4315 100644 --- a/packages/db/tests/query/includes.test.ts +++ b/packages/db/tests/query/includes.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { and, +import { + and, coalesce, concat, count, @@ -8,7 +9,8 @@ import { and, gte, innerJoin, materialize, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { localOnlyCollectionOptions } from '../../src/local-only.js' @@ -3457,9 +3459,13 @@ describe(`includes subqueries`, () => { joinedChildren: materialize( q .from({ joinedChild: children }) - .join(innerJoin({ metadata: metadataRows }, - ({ joinedChild, metadata }) => - eq(joinedChild.metadataId, metadata.id))) + .join( + innerJoin( + { metadata: metadataRows }, + ({ joinedChild, metadata }) => + eq(joinedChild.metadataId, metadata.id), + ), + ) .where(({ metadata }) => eq(metadata.groupId, parent.groupId)) .where(({ metadata }) => eq(metadata.tag, parent.tag)) .select(({ joinedChild }) => ({ id: joinedChild.id })), diff --git a/packages/db/tests/query/indexes.test.ts b/packages/db/tests/query/indexes.test.ts index 4517e3ec51..f28daa640e 100644 --- a/packages/db/tests/query/indexes.test.ts +++ b/packages/db/tests/query/indexes.test.ts @@ -390,8 +390,11 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join(leftJoin({ other: secondCollection }, ({ item, other }: any) => - eq(item.status, other.status))) + .join( + leftJoin({ other: secondCollection }, ({ item, other }: any) => + eq(item.status, other.status), + ), + ) .where(({ item }: any) => eq(item.age, 25)) .select(({ item, other }: any) => ({ id: item.id, @@ -499,8 +502,11 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join(leftJoin({ other: secondCollection }, ({ item, other }: any) => - eq(item.id, other.id))) + .join( + leftJoin({ other: secondCollection }, ({ item, other }: any) => + eq(item.id, other.id), + ), + ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -602,8 +608,11 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join(innerJoin({ other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2))) + .join( + innerJoin({ other: secondCollection }, ({ item, other }: any) => + eq(item.id, other.id2), + ), + ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -699,8 +708,11 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join(innerJoin({ other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2))) + .join( + innerJoin({ other: secondCollection }, ({ item, other }: any) => + eq(item.id, other.id2), + ), + ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -791,8 +803,11 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join(leftJoin({ other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2))) + .join( + leftJoin({ other: secondCollection }, ({ item, other }: any) => + eq(item.id, other.id2), + ), + ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -908,8 +923,11 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join(leftJoin({ other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2))) + .join( + leftJoin({ other: secondCollection }, ({ item, other }: any) => + eq(item.id, other.id2), + ), + ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -1003,8 +1021,11 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join(rightJoin({ other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2))) + .join( + rightJoin({ other: secondCollection }, ({ item, other }: any) => + eq(item.id, other.id2), + ), + ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) @@ -1094,8 +1115,11 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join(rightJoin({ other: secondCollection }, - ({ item, other }: any) => eq(item.id, other.id2))) + .join( + rightJoin({ other: secondCollection }, ({ item, other }: any) => + eq(item.id, other.id2), + ), + ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), ) diff --git a/packages/db/tests/query/ir-stable-identity-oracle.test.ts b/packages/db/tests/query/ir-stable-identity-oracle.test.ts index 0410892357..d422324ea4 100644 --- a/packages/db/tests/query/ir-stable-identity-oracle.test.ts +++ b/packages/db/tests/query/ir-stable-identity-oracle.test.ts @@ -871,7 +871,9 @@ function createAlphaRenamedJoinQuery( userAlias, ), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `inner`, from: new CollectionRef( postsCollection as unknown as ConstructorParameters< @@ -913,7 +915,9 @@ function createAlphaRenamedImplicitJoinQuery( userAlias, ), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `inner`, from: new CollectionRef( postsCollection as unknown as ConstructorParameters< @@ -1276,8 +1280,11 @@ const structuredQueries: Array<[string, () => QueryIR]> = [ getQueryIR( new Query() .from({ user: usersCollection }) - .join(leftJoin({ post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId))) + .join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .where(({ post }) => eq(post.published, true)) .select(({ user, post }) => ({ userId: user.id, @@ -1295,12 +1302,16 @@ const structuredQueries: Array<[string, () => QueryIR]> = [ .from({ post: postsCollection }) .where(({ post }) => gt(post.views, 100)), }) - .join(innerJoin({ - activeUser: new Query() - .from({ user: usersCollection }) - .where(({ user }) => eq(user.status, `active`)), - }, - ({ post, activeUser }) => eq(post.userId, activeUser.id))), + .join( + innerJoin( + { + activeUser: new Query() + .from({ user: usersCollection }) + .where(({ user }) => eq(user.status, `active`)), + }, + ({ post, activeUser }) => eq(post.userId, activeUser.id), + ), + ), ), ], [ diff --git a/packages/db/tests/query/join-result-key-oracle.property.test.ts b/packages/db/tests/query/join-result-key-oracle.property.test.ts index 092a3d55df..1f6dca0e06 100644 --- a/packages/db/tests/query/join-result-key-oracle.property.test.ts +++ b/packages/db/tests/query/join-result-key-oracle.property.test.ts @@ -42,7 +42,7 @@ */ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, it } from 'vitest' -import { createLiveQueryCollection, eq, joinOf } from '../../src/query/index.js' +import { createLiveQueryCollection, eq, joinOf } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { oraclePropertyOptions, diff --git a/packages/db/tests/query/join-subquery.test-d.ts b/packages/db/tests/query/join-subquery.test-d.ts index 0d88ec6574..92fc49f829 100644 --- a/packages/db/tests/query/join-subquery.test-d.ts +++ b/packages/db/tests/query/join-subquery.test-d.ts @@ -1,5 +1,11 @@ import { describe, expectTypeOf, test } from 'vitest' -import { createLiveQueryCollection, eq, gt, innerJoin, leftJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + gt, + innerJoin, + leftJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -99,8 +105,11 @@ describe(`Join Subquery Types`, () => { // Join subquery with users return q .from({ issue: project1Issues }) - .join(innerJoin({ user: usersCollection }, - ({ issue, user }) => eq(issue.userId, user.id))) + .join( + innerJoin({ user: usersCollection }, ({ issue, user }) => + eq(issue.userId, user.id), + ), + ) .select(({ issue, user }) => ({ issue_title: issue.title, user_name: user.name, @@ -135,8 +144,11 @@ describe(`Join Subquery Types`, () => { // Join all issues with active users subquery - no SELECT to test namespaced result return q .from({ issue: issuesCollection }) - .join(leftJoin({ activeUser: activeUsers }, - ({ issue, activeUser }) => eq(issue.userId, activeUser.id))) + .join( + leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), + ), + ) }, }) @@ -168,9 +180,13 @@ describe(`Join Subquery Types`, () => { // Join both subqueries return q .from({ longIssue: longIssues }) - .join(innerJoin({ activeUser: activeUsers }, - ({ longIssue, activeUser }) => - eq(longIssue.userId, activeUser.id))) + .join( + innerJoin( + { activeUser: activeUsers }, + ({ longIssue, activeUser }) => + eq(longIssue.userId, activeUser.id), + ), + ) .select(({ longIssue, activeUser }) => ({ issue_title: longIssue.title, issue_duration: longIssue.duration, @@ -206,8 +222,11 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: issuesCollection }) - .join(innerJoin({ engUser: engineeringUsers }, - ({ issue, engUser }) => eq(issue.userId, engUser.id))) + .join( + innerJoin({ engUser: engineeringUsers }, ({ issue, engUser }) => + eq(issue.userId, engUser.id), + ), + ) .select(({ issue, engUser }) => ({ issue_title: issue.title, user_name: engUser.name, @@ -239,8 +258,11 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: issuesCollection }) - .join(leftJoin({ activeUser: activeUsers }, - ({ issue, activeUser }) => eq(issue.userId, activeUser.id))) + .join( + leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), + ), + ) }, }) @@ -285,8 +307,11 @@ describe(`Join Subquery Types`, () => { // Join both transformed subqueries return q .from({ task: transformedIssues }) - .join(innerJoin({ profile: userProfiles }, - ({ task, profile }) => eq(task.assigneeId, profile.profileId))) + .join( + innerJoin({ profile: userProfiles }, ({ task, profile }) => + eq(task.assigneeId, profile.profileId), + ), + ) .select(({ task, profile }) => ({ id: task.taskId, name: task.taskName, @@ -328,8 +353,11 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: filteredIssues }) - .join(innerJoin({ user: usersCollection }, - ({ issue, user }) => eq(issue.userId, user.id))) + .join( + innerJoin({ user: usersCollection }, ({ issue, user }) => + eq(issue.userId, user.id), + ), + ) .select(({ issue, user }) => ({ // Should have access to all original Issue properties issue_id: issue.id, @@ -373,8 +401,11 @@ describe(`Join Subquery Types`, () => { // Join all issues with active users subquery with SELECT return q .from({ issue: issuesCollection }) - .join(leftJoin({ activeUser: activeUsers }, - ({ issue, activeUser }) => eq(issue.userId, activeUser.id))) + .join( + leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), + ), + ) .select(({ issue, activeUser }) => ({ issue_title: issue.title, user_name: activeUser.name, // Should now be string | undefined diff --git a/packages/db/tests/query/join-subquery.test.ts b/packages/db/tests/query/join-subquery.test.ts index 61139f646c..f5791185c0 100644 --- a/packages/db/tests/query/join-subquery.test.ts +++ b/packages/db/tests/query/join-subquery.test.ts @@ -1,5 +1,13 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { createLiveQueryCollection, eq, gt, innerJoin , joinOf, leftJoin, rightJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + gt, + innerJoin, + joinOf, + leftJoin, + rightJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' @@ -223,8 +231,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Join subquery with users return q .from({ issue: project1Issues }) - .join(innerJoin({ user: usersCollection }, - ({ issue, user }) => eq(issue.userId, user.id))) + .join( + innerJoin({ user: usersCollection }, ({ issue, user }) => + eq(issue.userId, user.id), + ), + ) .select(({ issue, user }) => ({ issue_title: issue.title, user_name: user.name, @@ -259,8 +270,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Join all issues with active users subquery return q .from({ issue: issuesCollection }) - .join(leftJoin({ activeUser: activeUsers }, - ({ issue, activeUser }) => eq(issue.userId, activeUser.id))) + .join( + leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), + ), + ) .select(({ issue, activeUser }) => ({ issue_title: issue.title, user_name: activeUser.name, @@ -304,9 +318,13 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Join both subqueries return q .from({ longIssue: longIssues }) - .join(innerJoin({ activeUser: activeUsers }, - ({ longIssue, activeUser }) => - eq(longIssue.userId, activeUser.id))) + .join( + innerJoin( + { activeUser: activeUsers }, + ({ longIssue, activeUser }) => + eq(longIssue.userId, activeUser.id), + ), + ) .select(({ longIssue, activeUser }) => ({ issue_title: longIssue.title, issue_duration: longIssue.duration, @@ -362,8 +380,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .join(innerJoin({ engUser: engineeringUsers }, - ({ issue, engUser }) => eq(issue.userId, engUser.id))) + .join( + innerJoin({ engUser: engineeringUsers }, ({ issue, engUser }) => + eq(issue.userId, engUser.id), + ), + ) .select(({ issue, engUser }) => ({ issue_title: issue.title, user_name: engUser.name, @@ -402,16 +423,24 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .join(joinOf(joinType, { user: activeUsers }, - ({ issue, user }) => eq(issue.userId, user.id))) + .join( + joinOf(joinType, { user: activeUsers }, ({ issue, user }) => + eq(issue.userId, user.id), + ), + ) .where(({ user }) => eq(user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) } return q .from({ issue: issuesCollection }) - .join(joinOf(joinType, { user: usersCollection }, - ({ issue, user }) => eq(issue.userId, user.id))) + .join( + joinOf( + joinType, + { user: usersCollection }, + ({ issue, user }) => eq(issue.userId, user.id), + ), + ) .where(({ user }) => eq(user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -431,8 +460,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .join(innerJoin({ user: activeUsers }, ({ issue, user }) => - eq(issue.userId, user.id))) + .join( + innerJoin({ user: activeUsers }, ({ issue, user }) => + eq(issue.userId, user.id), + ), + ) .where(({ user }) => eq(user.name, `Charlie`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -451,8 +483,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .join(innerJoin({ member: activeUsers }, ({ issue, member }) => - eq(issue.userId, member.id))) + .join( + innerJoin({ member: activeUsers }, ({ issue, member }) => + eq(issue.userId, member.id), + ), + ) .where(({ member }) => eq(member.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -467,13 +502,20 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { query: (q) => { const usersWithProfiles = q .from({ user: usersCollection }) - .join(innerJoin({ profile: profilesCollection }, ({ user, profile }) => - eq(user.id, profile.userId))) + .join( + innerJoin( + { profile: profilesCollection }, + ({ user, profile }) => eq(user.id, profile.userId), + ), + ) return q .from({ issue: issuesCollection }) - .join(innerJoin({ member: usersWithProfiles }, ({ issue, member }) => - eq(issue.userId, member.user.id))) + .join( + innerJoin({ member: usersWithProfiles }, ({ issue, member }) => + eq(issue.userId, member.user.id), + ), + ) .where(({ member }) => eq(member.user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -488,14 +530,21 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { query: (q) => { const usersWithProfiles = q .from({ user: usersCollection }) - .join(innerJoin({ profile: profilesCollection }, ({ user, profile }) => - eq(user.id, profile.userId))) + .join( + innerJoin( + { profile: profilesCollection }, + ({ user, profile }) => eq(user.id, profile.userId), + ), + ) .select(({ user }) => user) return q .from({ issue: issuesCollection }) - .join(innerJoin({ member: usersWithProfiles }, ({ issue, member }) => - eq(issue.userId, member.id))) + .join( + innerJoin({ member: usersWithProfiles }, ({ issue, member }) => + eq(issue.userId, member.id), + ), + ) .where(({ member }) => eq(member.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -515,8 +564,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .join(leftJoin({ activeUser: activeUsers }, - ({ issue, activeUser }) => eq(issue.userId, activeUser.id))) + .join( + leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), + ), + ) .select(({ issue, activeUser }) => ({ issue_title: issue.title, issue_status: issue.status, @@ -550,12 +602,16 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { query: (q) => { return q .from({ product: productsCollection }) - .join(leftJoin({ - tried: q - .from({ tried: trialsCollection }) - .where(({ tried }) => eq(tried.userId, 1)), - }, - ({ tried, product }) => eq(tried.productId, product.id))) + .join( + leftJoin( + { + tried: q + .from({ tried: trialsCollection }) + .where(({ tried }) => eq(tried.userId, 1)), + }, + ({ tried, product }) => eq(tried.productId, product.id), + ), + ) .where(({ product }) => eq(product.id, 1)) .select(({ product, tried }) => ({ product, @@ -585,14 +641,18 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { query: (q) => { return q .from({ issue: issuesCollection }) - .join(leftJoin({ - users: q - .from({ user: usersCollection }) - .where(({ user }) => eq(user.status, `active`)) - .orderBy(({ user }) => user.name, `asc`) - .limit(1), - }, - ({ issue, users }) => eq(issue.userId, users.id))) + .join( + leftJoin( + { + users: q + .from({ user: usersCollection }) + .where(({ user }) => eq(user.status, `active`)) + .orderBy(({ user }) => user.name, `asc`) + .limit(1), + }, + ({ issue, users }) => eq(issue.userId, users.id), + ), + ) .orderBy(({ issue }) => issue.id, `desc`) .limit(1) }, @@ -632,8 +692,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { .orderBy(({ user }) => user.name, `asc`) .limit(1), }) - .join(rightJoin({ issue: issuesCollection }, - ({ issue, users }) => eq(issue.userId, users.id))) + .join( + rightJoin({ issue: issuesCollection }, ({ issue, users }) => + eq(issue.userId, users.id), + ), + ) .orderBy(({ issue }) => issue.id, `desc`) .limit(1) }, @@ -691,8 +754,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Join both transformed subqueries return q .from({ task: transformedIssues }) - .join(innerJoin({ profile: userProfiles }, - ({ task, profile }) => eq(task.assigneeId, profile.profileId))) + .join( + innerJoin({ profile: userProfiles }, ({ task, profile }) => + eq(task.assigneeId, profile.profileId), + ), + ) .select(({ task, profile }) => ({ id: task.taskId, name: task.taskName, @@ -767,8 +833,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ user: activeUsers }) - .join(innerJoin({ issue: issuesCollection }, ({ user, issue }) => - eq(user.id, issue.userId))) + .join( + innerJoin({ issue: issuesCollection }, ({ user, issue }) => + eq(user.id, issue.userId), + ), + ) .where(({ user }) => eq(user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) }, @@ -803,8 +872,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ row: allIssues }) - .join(innerJoin({ user: usersCollection }, ({ row, user }) => - eq(row.userId, user.id))) + .join( + innerJoin({ user: usersCollection }, ({ row, user }) => + eq(row.userId, user.id), + ), + ) .where(({ row }) => eq(row.status, `open`)) .select(({ row }) => ({ id: row.id })) }, @@ -833,8 +905,12 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Filter for active users at the subquery level to avoid WHERE on SELECT fields bug const activeUsersWithProfiles = q .from({ user: usersCollection }) - .join(innerJoin({ profile: profilesCollection }, - ({ user, profile }) => eq(user.id, profile.userId))) + .join( + innerJoin( + { profile: profilesCollection }, + ({ user, profile }) => eq(user.id, profile.userId), + ), + ) .where(({ user }) => eq(user.status, `active`)) .select(({ user, profile }) => ({ userId: user.id, @@ -849,8 +925,13 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // This tests that aliasRemapping['activeUser'] = 'user' (flattened to innermost) return q .from({ activeUser: activeUsersWithProfiles }) - .join(innerJoin({ issue: issuesCollection }, - ({ activeUser, issue }) => eq(issue.userId, activeUser.userId))) + .join( + innerJoin( + { issue: issuesCollection }, + ({ activeUser, issue }) => + eq(issue.userId, activeUser.userId), + ), + ) .select(({ activeUser, issue }) => ({ issue_title: issue.title, issue_status: issue.status, @@ -910,8 +991,12 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Level 1: Subquery WITH a join (user + profile) const usersWithProfiles = q .from({ user: usersCollection }) - .join(innerJoin({ profile: profilesCollection }, - ({ user, profile }) => eq(user.id, profile.userId))) + .join( + innerJoin( + { profile: profilesCollection }, + ({ user, profile }) => eq(user.id, profile.userId), + ), + ) .where(({ user }) => eq(user.status, `active`)) .select(({ user, profile }) => ({ userId: user.id, @@ -924,8 +1009,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // This tests that aliasRemapping['author'] = 'user' for lazy loading return q .from({ issue: issuesCollection }) - .join(leftJoin({ author: usersWithProfiles }, - ({ issue, author }) => eq(issue.userId, author.userId))) + .join( + leftJoin({ author: usersWithProfiles }, ({ issue, author }) => + eq(issue.userId, author.userId), + ), + ) .select(({ issue, author }) => ({ issue_id: issue.id, issue_title: issue.title, @@ -958,8 +1046,12 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Level 1: Base joined subquery (user + profile) const usersWithProfiles = q .from({ user: usersCollection }) - .join(innerJoin({ profile: profilesCollection }, - ({ user, profile }) => eq(user.id, profile.userId))) + .join( + innerJoin( + { profile: profilesCollection }, + ({ user, profile }) => eq(user.id, profile.userId), + ), + ) .select(({ user, profile }) => ({ userId: user.id, userName: user.name, @@ -982,8 +1074,12 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { // Tests that aliasRemapping['author'] = 'user' (flattened to innermost, not 'userProfile') return q .from({ issue: issuesCollection }) - .join(innerJoin({ author: activeUsersWithProfiles }, - ({ issue, author }) => eq(issue.userId, author.id))) + .join( + innerJoin( + { author: activeUsersWithProfiles }, + ({ issue, author }) => eq(issue.userId, author.id), + ), + ) .select(({ issue, author }) => ({ issue_title: issue.title, author_name: author.name, @@ -1037,8 +1133,11 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { const right = shared ? left : activeUsers() let query = q .from({ leftUser: left }) - .join(innerJoin({ rightUser: right }, ({ leftUser, rightUser }) => - eq(leftUser.id, rightUser.id))) + .join( + innerJoin({ rightUser: right }, ({ leftUser, rightUser }) => + eq(leftUser.id, rightUser.id), + ), + ) if (filterRight) { query = query.where(({ rightUser }) => @@ -1113,14 +1212,20 @@ describe(`Lazy join: subquery whose join key resolves to an indexed collection`, query: (q) => { const teamByMember = q .from({ member: members }) - .join(leftJoin({ team: teams }, ({ team, member }) => - eq(team.id, member.teamId))) + .join( + leftJoin({ team: teams }, ({ team, member }) => + eq(team.id, member.teamId), + ), + ) .select(({ team }) => ({ teamId: team.id })) return q .from({ m: members }) - .join(leftJoin({ memberTeam: teamByMember }, ({ m, memberTeam }) => - eq(memberTeam.teamId, m.teamId))) + .join( + leftJoin({ memberTeam: teamByMember }, ({ m, memberTeam }) => + eq(memberTeam.teamId, m.teamId), + ), + ) .select(({ m }) => ({ id: m.id })) }, }) @@ -1177,8 +1282,11 @@ describe(`Lazy join index availability`, () => { const live = createLiveQueryCollection((q) => q .from({ team: teams }) - .join(leftJoin({ member: members }, ({ team, member }) => - eq(team.id, member.teamId))) + .join( + leftJoin({ member: members }, ({ team, member }) => + eq(team.id, member.teamId), + ), + ) .select(({ team, member }) => ({ id: team.id, memberId: member.id, @@ -1232,8 +1340,11 @@ describe(`Lazy join index availability`, () => { const live = createLiveQueryCollection((q) => q .from({ team: teams }) - .join(leftJoin({ member: members }, ({ team, member }) => - eq(team.id, member.teamId))) + .join( + leftJoin({ member: members }, ({ team, member }) => + eq(team.id, member.teamId), + ), + ) .select(({ team, member }) => ({ id: team.id, memberId: member.id, diff --git a/packages/db/tests/query/join.test-d.ts b/packages/db/tests/query/join.test-d.ts index 8320187c9d..4e1fa8f63a 100644 --- a/packages/db/tests/query/join.test-d.ts +++ b/packages/db/tests/query/join.test-d.ts @@ -1,7 +1,14 @@ import { describe, expectTypeOf, test } from 'vitest' import { z } from 'zod' import { type } from 'arktype' -import { createLiveQueryCollection, eq, fullJoin, innerJoin, leftJoin, rightJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + fullJoin, + innerJoin, + leftJoin, + rightJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, omitVirtualProps } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -52,8 +59,11 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(innerJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))), + .join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ), }) const results = innerJoinQuery.toArray @@ -77,8 +87,11 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))), + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ), }) const results = leftJoinQuery.toArray @@ -102,8 +115,11 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(rightJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))), + .join( + rightJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ), }) const results = rightJoinQuery.toArray @@ -127,8 +143,11 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(fullJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))), + .join( + fullJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ), }) const results = fullJoinQuery.toArray @@ -167,10 +186,16 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) - .join(rightJoin({ project: projectsCollection }, - ({ user, project }) => eq(user.id, project.user_id))), + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) + .join( + rightJoin({ project: projectsCollection }, ({ user, project }) => + eq(user.id, project.user_id), + ), + ), }) const results = multipleJoinQuery.toArray @@ -198,8 +223,11 @@ describe(`Join Types - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, // This should still be accessible in select @@ -231,8 +259,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))), + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ), }) const results = leftJoinQuery.toArray @@ -256,8 +287,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(rightJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))), + .join( + rightJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ), }) const results = rightJoinQuery.toArray @@ -281,8 +315,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))), + .join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ), }) const results = innerJoinQuery.toArray @@ -306,8 +343,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(fullJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))), + .join( + fullJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ), }) const results = fullJoinQuery.toArray @@ -346,10 +386,16 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) - .join(rightJoin({ project: projectsCollection }, ({ user, project }) => - eq(user.id, project.user_id))), + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) + .join( + rightJoin({ project: projectsCollection }, ({ user, project }) => + eq(user.id, project.user_id), + ), + ), }) const results = multipleJoinQuery.toArray @@ -377,8 +423,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, // This should be string | undefined due to left join @@ -408,8 +457,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, // This should be string (not undefined) due to inner join @@ -453,10 +505,16 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) - .join(innerJoin({ project: projectsCollection }, - ({ dept, project }) => eq(dept.id, project.department_id))), + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) + .join( + innerJoin({ project: projectsCollection }, ({ dept, project }) => + eq(dept.id, project.department_id), + ), + ), }) const results = mixedJoinQuery.toArray @@ -510,8 +568,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .join(innerJoin({ user: userCollection }, - ({ event, user }) => eq(event.user_id, user.id))) + .join( + innerJoin({ user: userCollection }, ({ event, user }) => + eq(event.user_id, user.id), + ), + ) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -523,8 +584,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .join(innerJoin({ user: userCollection }, - ({ event, user }) => eq(user.id, event.user_id))) + .join( + innerJoin({ user: userCollection }, ({ event, user }) => + eq(user.id, event.user_id), + ), + ) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -595,8 +659,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .join(innerJoin({ user: userCollection }, - ({ event, user }) => eq(event.user_id, user.id))) + .join( + innerJoin({ user: userCollection }, ({ event, user }) => + eq(event.user_id, user.id), + ), + ) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -608,8 +675,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .join(innerJoin({ user: userCollection }, - ({ event, user }) => eq(user.id, event.user_id))) + .join( + innerJoin({ user: userCollection }, ({ event, user }) => + eq(user.id, event.user_id), + ), + ) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -680,8 +750,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ post: postCollection }) - .join(leftJoin({ user: userCollection }, - ({ post, user }) => eq(post.author_id, user.id))) + .join( + leftJoin({ user: userCollection }, ({ post, user }) => + eq(post.author_id, user.id), + ), + ) .select(({ post, user }) => ({ postTitle: post.title, authorName: user.name, // This will be string | undefined due to left join @@ -709,8 +782,11 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ userName: user.name, // dept is a proxy ref that is always present at build time, @@ -743,8 +819,11 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join(rightJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + rightJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ // user is the nullable side in a right join userName: user.name, @@ -772,8 +851,11 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join(fullJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + fullJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, @@ -801,8 +883,11 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join(innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, @@ -834,8 +919,11 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ userName: user.name, // dept is a proxy ref that is always present at build time, @@ -866,8 +954,11 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join(rightJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + rightJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ // user is the nullable side in a right join userName: user.name, @@ -893,8 +984,11 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join(fullJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + fullJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, @@ -920,8 +1014,11 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join(innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ userName: user.name, deptName: dept.name, @@ -985,8 +1082,11 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ event: eventCollection }) - .join(innerJoin({ user: userCollection }, - ({ event, user }) => eq(event.user_id, user.id))) + .join( + innerJoin({ user: userCollection }, ({ event, user }) => + eq(event.user_id, user.id), + ), + ) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -998,8 +1098,11 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ event: eventCollection }) - .join(innerJoin({ user: userCollection }, - ({ event, user }) => eq(user.id, event.user_id))) + .join( + innerJoin({ user: userCollection }, ({ event, user }) => + eq(user.id, event.user_id), + ), + ) .select(({ event, user }) => ({ eventTitle: event.title, userName: user.name, @@ -1070,8 +1173,11 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ post: postCollection }) - .join(leftJoin({ user: userCollection }, - ({ post, user }) => eq(post.author_id, user.id))) + .join( + leftJoin({ user: userCollection }, ({ post, user }) => + eq(post.author_id, user.id), + ), + ) .select(({ post, user }) => ({ postTitle: post.title, authorName: user.name, // This will be string | undefined due to left join @@ -1135,8 +1241,11 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ post: postCollection }) - .join(innerJoin({ user: userCollection }, ({ post, user }) => - eq(post.user_id, user.id))) + .join( + innerJoin({ user: userCollection }, ({ post, user }) => + eq(post.user_id, user.id), + ), + ) .select(({ post, user }) => ({ postTitle: post.title, userName: user.name, diff --git a/packages/db/tests/query/join.test.ts b/packages/db/tests/query/join.test.ts index 5c65fbacab..9bf94c728d 100644 --- a/packages/db/tests/query/join.test.ts +++ b/packages/db/tests/query/join.test.ts @@ -1,6 +1,11 @@ import { beforeEach, describe, expect, test } from 'vitest' import { Temporal } from 'temporal-polyfill' -import { concat, createLiveQueryCollection, eq, fullJoin, gt , +import { + concat, + createLiveQueryCollection, + eq, + fullJoin, + gt, inArray, innerJoin, isNull, @@ -10,7 +15,8 @@ import { concat, createLiveQueryCollection, eq, fullJoin, gt , lt, not, or, - rightJoin } from '../../src/query/index.js' + rightJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { flushPromises, @@ -118,8 +124,13 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join(joinOf(joinType, { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + joinOf( + joinType, + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -203,8 +214,13 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join(joinOf(joinType, { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))), + .join( + joinOf( + joinType, + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + ), + ), }) const results = joinQuery.toArray as Array< @@ -293,8 +309,13 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join(joinOf(joinType, { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + joinOf( + joinType, + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -333,8 +354,13 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join(joinOf(joinType, { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + joinOf( + joinType, + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -369,8 +395,13 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join(joinOf(joinType, { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + joinOf( + joinType, + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -415,8 +446,13 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ user: usersCollection }) - .join(joinOf(joinType, { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + joinOf( + joinType, + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -515,8 +551,12 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .join(leftJoin({ member: teamMembersCollection }, ({ team, member }) => - eq(team.id, member.team_id))) + .join( + leftJoin( + { member: teamMembersCollection }, + ({ team, member }) => eq(team.id, member.team_id), + ), + ) .where(({ member }) => eq(member.user_id, 100)) .select(({ team, member }) => ({ team_id: team.id, @@ -533,8 +573,12 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .join(rightJoin({ member: teamMembersCollection }, - ({ team, member }) => eq(team.id, member.team_id))) + .join( + rightJoin( + { member: teamMembersCollection }, + ({ team, member }) => eq(team.id, member.team_id), + ), + ) .where(({ team }) => eq(team.active, true)) .select(({ team, member }) => ({ team_id: team.id, @@ -551,8 +595,12 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .join(fullJoin({ member: teamMembersCollection }, ({ team, member }) => - eq(team.id, member.team_id))) + .join( + fullJoin( + { member: teamMembersCollection }, + ({ team, member }) => eq(team.id, member.team_id), + ), + ) .where(({ member }) => eq(member.role, `admin`)) .select(({ team, member }) => ({ team_id: team.id, @@ -569,8 +617,12 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .join(innerJoin({ member: teamMembersCollection }, - ({ team, member }) => eq(team.id, member.team_id))) + .join( + innerJoin( + { member: teamMembersCollection }, + ({ team, member }) => eq(team.id, member.team_id), + ), + ) .where(({ member }) => eq(member.user_id, 100)) .select(({ team, member }) => ({ team_id: team.id, @@ -708,10 +760,18 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ company: companiesCollection }) - .join(joinOf(joinType, { project: projectsCollection }, - ({ company, project }) => eq(project.company_id, company.id))) - .join(joinOf(joinType, { task: tasksCollection }, - ({ task, project }) => eq(task.project_id, project.id))) + .join( + joinOf( + joinType, + { project: projectsCollection }, + ({ company, project }) => eq(project.company_id, company.id), + ), + ) + .join( + joinOf(joinType, { task: tasksCollection }, ({ task, project }) => + eq(task.project_id, project.id), + ), + ) .select(({ company, project, task }) => ({ company_name: company.name, project_name: project.name, @@ -791,8 +851,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: usersCollection }) - .join(innerJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -863,8 +926,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: emptyUsers }) - .join(innerJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -899,8 +965,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_id: user.id, user_name: user.name, @@ -932,12 +1001,14 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: usersCollection }) - .join(innerJoin({ dept: departmentsCollection }, - ({ user, dept }) => + .join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => eq( concat(`dept`, user.department_id), concat(`dept`, dept.id), - ))) + ), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -988,8 +1059,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ left: leftCollection }) - .join(innerJoin({ right: rightCollection }, ({ left, right }) => - eq(left.joinedAt, right.joinedAt))) + .join( + innerJoin({ right: rightCollection }, ({ left, right }) => + eq(left.joinedAt, right.joinedAt), + ), + ) .select(({ left, right }) => ({ leftId: left.id, rightId: right.id, @@ -1039,8 +1113,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ left: leftCollection }) - .join(innerJoin({ right: rightCollection }, ({ left, right }) => - eq(left.joinedAt, right.joinedAt))) + .join( + innerJoin({ right: rightCollection }, ({ left, right }) => + eq(left.joinedAt, right.joinedAt), + ), + ) .select(({ left, right }) => ({ leftId: left.id, rightId: right.id, @@ -1144,10 +1221,17 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ player: playersCollection }) - .join(innerJoin({ client: clientsCollection }, ({ client, player }) => - eq(client.player, player.name))) - .join(innerJoin({ balance: balancesCollection }, ({ balance, client }) => - eq(balance.client, client.name))) + .join( + innerJoin({ client: clientsCollection }, ({ client, player }) => + eq(client.player, player.name), + ), + ) + .join( + innerJoin( + { balance: balancesCollection }, + ({ balance, client }) => eq(balance.client, client.name), + ), + ) .select(({ player, client, balance }) => ({ player_name: player.name, client_name: client.name, @@ -1290,8 +1374,12 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ users: selfJoinUsersCollection }) - .join(innerJoin({ parentUsers: selfJoinUsersCollection }, - ({ users, parentUsers }) => eq(users.parentId, parentUsers.id))) + .join( + innerJoin( + { parentUsers: selfJoinUsersCollection }, + ({ users, parentUsers }) => eq(users.parentId, parentUsers.id), + ), + ) .select(({ users, parentUsers }) => ({ user_id: users.id, user_name: users.name, @@ -1359,8 +1447,12 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ users: bidirectionalUsersCollection }) - .join(innerJoin({ parentUsers: bidirectionalUsersCollection }, - ({ users, parentUsers }) => eq(users.parentId, parentUsers.id))) + .join( + innerJoin( + { parentUsers: bidirectionalUsersCollection }, + ({ users, parentUsers }) => eq(users.parentId, parentUsers.id), + ), + ) .select(({ users, parentUsers }) => ({ user_name: users.name, parent_name: parentUsers.name, @@ -1378,8 +1470,12 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ users: bidirectionalUsersCollection }) - .join(innerJoin({ parentUsers: bidirectionalUsersCollection }, - ({ users, parentUsers }) => eq(parentUsers.id, users.parentId))) + .join( + innerJoin( + { parentUsers: bidirectionalUsersCollection }, + ({ users, parentUsers }) => eq(parentUsers.id, users.parentId), + ), + ) .select(({ users, parentUsers }) => ({ user_name: users.name, parent_name: parentUsers.name, @@ -1419,8 +1515,13 @@ function createJoinTests(autoIndex: `off` | `eager`): void { createLiveQueryCollection({ startSync: true, query: (q) => - q.from({ user: usersCollection }).join(innerJoin({ dept: departmentsCollection }, - ({ user }) => eq(user.id, user.department_id))), + q + .from({ user: usersCollection }) + .join( + innerJoin({ dept: departmentsCollection }, ({ user }) => + eq(user.id, user.department_id), + ), + ), }) }).toThrow( `Invalid join condition: both expressions refer to the same source "user"`, @@ -1442,8 +1543,9 @@ function createJoinTests(autoIndex: `off` | `eager`): void { createLiveQueryCollection({ startSync: true, query: (q) => - q.from({ user: usersCollection }).join(innerJoin({ dept: departmentsCollection }, - () => eq(1, 2))), + q + .from({ user: usersCollection }) + .join(innerJoin({ dept: departmentsCollection }, () => eq(1, 2))), }) }).toThrow( `Invalid join condition: expressions must reference source aliases`, @@ -1469,10 +1571,16 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: usersCollection }) - .join(innerJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(dept.id, user.department_id))) - .join(innerJoin({ dept2: departmentsCollection2 }, - ({ user, dept }) => eq(dept.id, user.department_id))), + .join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(dept.id, user.department_id), + ), + ) + .join( + innerJoin({ dept2: departmentsCollection2 }, ({ user, dept }) => + eq(dept.id, user.department_id), + ), + ), }) }).toThrow( `Invalid join condition: right expression does not refer to the joined source "dept2"`, @@ -1494,8 +1602,13 @@ function createJoinTests(autoIndex: `off` | `eager`): void { createLiveQueryCollection({ startSync: true, query: (q) => - q.from({ user: usersCollection }).join(innerJoin({ dept: departmentsCollection }, - ({ user }) => eq(user.id, user.department_id))), + q + .from({ user: usersCollection }) + .join( + innerJoin({ dept: departmentsCollection }, ({ user }) => + eq(user.id, user.department_id), + ), + ), }) }).toThrow( `Invalid join condition: both expressions refer to the same source "user"`, @@ -1517,8 +1630,12 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ special: specialUsersCollection }, - ({ user, special }) => eq(user.id, special.id))) + .join( + leftJoin( + { special: specialUsersCollection }, + ({ user, special }) => eq(user.id, special.id), + ), + ) .where(({ special }) => isUndefined(special)), }) @@ -1585,8 +1702,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { .where(({ event }) => eq(event.id, `3770a4a6-3260-4566-9f79-f50864ebdd46`), ) - .join(leftJoin({ parent: eventCollection }, - ({ event, parent }) => eq(parent.id, event.parent_id))) + .join( + leftJoin({ parent: eventCollection }, ({ event, parent }) => + eq(parent.id, event.parent_id), + ), + ) .select(({ event, parent }) => ({ id: event.id, parent_id: event.parent_id, @@ -1647,8 +1767,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ employee: peopleCollection }) .where(({ employee }) => gt(employee.age, 30)) - .join(leftJoin({ manager: peopleCollection }, - ({ employee, manager }) => eq(employee.manager_id, manager.id))) + .join( + leftJoin({ manager: peopleCollection }, ({ employee, manager }) => + eq(employee.manager_id, manager.id), + ), + ) .where(({ manager }) => or(isNull(manager.id), gt(manager.age, 35))) .select(({ employee, manager }) => ({ employeeId: employee.id, @@ -1733,18 +1856,26 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ main: collection1 }) - .join(leftJoin({ - join1: q - .from({ join1: collection2 }) - .where(({ join1 }) => not(gt(join1.other, 20))), - }, - ({ main, join1 }) => eq(main.value, join1.value))) - .join(leftJoin({ - join2: q - .from({ join2: collection2 }) - .where(({ join2 }) => not(lt(join2.other, 20))), - }, - ({ main, join2 }) => eq(main.value, join2.value))), + .join( + leftJoin( + { + join1: q + .from({ join1: collection2 }) + .where(({ join1 }) => not(gt(join1.other, 20))), + }, + ({ main, join1 }) => eq(main.value, join1.value), + ), + ) + .join( + leftJoin( + { + join2: q + .from({ join2: collection2 }) + .where(({ join2 }) => not(lt(join2.other, 20))), + }, + ({ main, join2 }) => eq(main.value, join2.value), + ), + ), }) const multipleResults = multipleJoinQuery.toArray @@ -1814,7 +1945,9 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ l: leftCollection }) - .join(leftJoin({ r: rightCollection }, ({ l, r }) => eq(l.rightId, r.id))) + .join( + leftJoin({ r: rightCollection }, ({ l, r }) => eq(l.rightId, r.id)), + ) .where(({ r }) => isUndefined(r.payload)) .select(({ l, r }) => ({ leftId: l.id, right: r })), }) @@ -1908,10 +2041,16 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ player: playersCollection }) - .join(leftJoin({ client: clientsCollection }, - ({ client, player }) => eq(client.player, player.name))) - .join(leftJoin({ balance: balancesCollection }, - ({ balance, client }) => eq(balance.client, client.name))) + .join( + leftJoin({ client: clientsCollection }, ({ client, player }) => + eq(client.player, player.name), + ), + ) + .join( + leftJoin({ balance: balancesCollection }, ({ balance, client }) => + eq(balance.client, client.name), + ), + ) .select(({ player, client, balance }) => ({ player_name: player.name, client_name: client.name, @@ -2001,8 +2140,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ task: taskCollection }) .where(({ task }) => inArray(task.id, [1])) - .join(innerJoin({ project: projectCollection }, ({ task, project }) => - eq(task.project_id, project.id))) + .join( + innerJoin({ project: projectCollection }, ({ task, project }) => + eq(task.project_id, project.id), + ), + ) .select(({ task, project }) => ({ task, project, diff --git a/packages/db/tests/query/live-query-collection.test.ts b/packages/db/tests/query/live-query-collection.test.ts index 3d6ea6484a..4b7790a61c 100644 --- a/packages/db/tests/query/live-query-collection.test.ts +++ b/packages/db/tests/query/live-query-collection.test.ts @@ -1,12 +1,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Temporal } from 'temporal-polyfill' -import { and, coalesce, +import { + and, + coalesce, createLiveQueryCollection, eq, ilike, innerJoin, leftJoin, - liveQueryCollectionOptions } from '../../src/query/index.js' + liveQueryCollectionOptions, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { Query } from '../../src/query/builder/index.js' import { @@ -913,9 +916,11 @@ describe(`createLiveQueryCollection`, () => { query: (q: any) => q .from({ thread: sourceLQ }) - .join(innerJoin({ label: labelsByEmail }, - ({ thread, label }: any) => - eq(thread.last_email_id, label.email_id))) + .join( + innerJoin({ label: labelsByEmail }, ({ thread, label }: any) => + eq(thread.last_email_id, label.email_id), + ), + ) .orderBy(({ thread }: any) => thread.last_sent_at, { direction: `desc`, }), @@ -1103,10 +1108,18 @@ describe(`createLiveQueryCollection`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ player: playerCollection }) - .join(leftJoin({ challenge1: challenge1Collection }, - ({ player, challenge1 }) => eq(player.id, challenge1.id))) - .join(leftJoin({ challenge2: challenge2Collection }, - ({ player, challenge2 }) => eq(player.id, challenge2.id))), + .join( + leftJoin( + { challenge1: challenge1Collection }, + ({ player, challenge1 }) => eq(player.id, challenge1.id), + ), + ) + .join( + leftJoin( + { challenge2: challenge2Collection }, + ({ player, challenge2 }) => eq(player.id, challenge2.id), + ), + ), ) // Start the query, but don't wait it, we are doing to write the data to the @@ -1963,8 +1976,11 @@ describe(`createLiveQueryCollection`, () => { const live = createLiveQueryCollection((q) => q .from({ user: users }) - .join(leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId))) + .join( + leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), + ), + ) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2008,8 +2024,11 @@ describe(`createLiveQueryCollection`, () => { const live = createLiveQueryCollection((q) => q .from({ user: users }) - .join(leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId))) + .join( + leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), + ), + ) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2106,8 +2125,11 @@ describe(`createLiveQueryCollection`, () => { const live = createLiveQueryCollection((q) => q .from({ parent: parents }) - .join(leftJoin({ child: children }, ({ parent, child }) => - eq(parent.id, child.parentId))) + .join( + leftJoin({ child: children }, ({ parent, child }) => + eq(parent.id, child.parentId), + ), + ) .orderBy(({ parent }) => parent.rank, `asc`) .limit(1) .select(({ parent, child }) => ({ @@ -3965,8 +3987,11 @@ describe(`createLiveQueryCollection`, () => { const userPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(innerJoin({ post: posts }, - ({ user, post }) => eq(user.id, post.authorId))) + .join( + innerJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.authorId), + ), + ) .where(({ user, post }) => and(eq(user.active, true), eq(post.published, true)), ) @@ -4472,7 +4497,11 @@ describe(`createLiveQueryCollection`, () => { query: (q) => q .from({ base }) - .join(leftJoin({ related }, ({ base: b, related: r }) => eq(b.id, r.id))) + .join( + leftJoin({ related }, ({ base: b, related: r }) => + eq(b.id, r.id), + ), + ) .select(({ base: b, related: r }) => ({ id: b.id, name: b.name, @@ -4513,7 +4542,11 @@ describe(`createLiveQueryCollection`, () => { query: (q) => q .from({ comments }) - .join(leftJoin({ users }, ({ comments: c, users: u }) => eq(c.userId, u.id))) + .join( + leftJoin({ users }, ({ comments: c, users: u }) => + eq(c.userId, u.id), + ), + ) .select(({ comments: c, users: u }) => ({ id: c.id, userId: coalesce(u.id, c.userId), @@ -4906,8 +4939,11 @@ describe(`createLiveQueryCollection`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ task: taskCollection }) - .join(leftJoin({ project: projectCollection }, ({ task, project }) => - eq(task.project_id, project.id))), + .join( + leftJoin({ project: projectCollection }, ({ task, project }) => + eq(task.project_id, project.id), + ), + ), ) const preloadPromise = liveQuery.preload() @@ -5021,8 +5057,11 @@ describe(`createLiveQueryCollection`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ task: taskCollection }) - .join(leftJoin({ project: projectCollection }, ({ task, project }) => - eq(task.project_id, project.id))), + .join( + leftJoin({ project: projectCollection }, ({ task, project }) => + eq(task.project_id, project.id), + ), + ), ) const preloadPromise = liveQuery.preload() @@ -5105,8 +5144,11 @@ describe(`createLiveQueryCollection`, () => { query: (q) => q .from({ team: teamsCollection }) - .join(leftJoin({ user: usersCollection }, ({ team, user }) => - eq(team.lead_id, user.id))) + .join( + leftJoin({ user: usersCollection }, ({ team, user }) => + eq(team.lead_id, user.id), + ), + ) .select(({ team, user }) => ({ teamName: team.name, leadName: user.name, diff --git a/packages/db/tests/query/load-subset-join-dedupe.test.ts b/packages/db/tests/query/load-subset-join-dedupe.test.ts index 1e1a55e4fb..c0e40daf1f 100644 --- a/packages/db/tests/query/load-subset-join-dedupe.test.ts +++ b/packages/db/tests/query/load-subset-join-dedupe.test.ts @@ -1,5 +1,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { createLiveQueryCollection, eq, leftJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + leftJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BasicIndex } from '../../src/indexes/basic-index.js' import { extractSimpleComparisons } from '../../src/query/expression-helpers.js' @@ -92,8 +96,11 @@ function createJoinedQuery( const live = createLiveQueryCollection((query) => query .from({ parent: parentCollection }) - .join(leftJoin({ child: childCollection }, ({ parent, child }) => - eq(child.parentId, parent.id))), + .join( + leftJoin({ child: childCollection }, ({ parent, child }) => + eq(child.parentId, parent.id), + ), + ), ) cleanups.push(() => live.cleanup()) return live diff --git a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts index 0affb65674..08d07d7ef1 100644 --- a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts +++ b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts @@ -1,11 +1,14 @@ import fc from 'fast-check' import { describe, expect, it, vi } from 'vitest' -import { createLiveQueryCollection, eq, +import { + createLiveQueryCollection, + eq, innerJoin, isUndefined, leftJoin, not, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { BasicIndex } from '../../src/indexes/basic-index.js' @@ -924,9 +927,13 @@ describe(`loadSubset replay refinement`, () => { const live = createLiveQueryCollection((q) => q .from({ primary: primary.collection }) - .join(innerJoin({ secondary: secondary.collection }, - ({ primary: left, secondary: right }) => - eq(left.joinKey, right.joinKey))) + .join( + innerJoin( + { secondary: secondary.collection }, + ({ primary: left, secondary: right }) => + eq(left.joinKey, right.joinKey), + ), + ) .orderBy(({ primary: row }) => row.version) .limit(1) .select(({ primary: left, secondary: right }) => ({ @@ -1585,12 +1592,18 @@ async function checkOrderedReplay( const from = q.from({ row: source }) const filtered = scenario.joined === `inner` - ? from.join(innerJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId))) + ? from.join( + innerJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId), + ), + ) : scenario.joined ? from - .join(leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId))) + .join( + leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId), + ), + ) .where(({ marker }) => scenario.joined === `none` ? isUndefined(marker.rowId) diff --git a/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts b/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts index f945eeb9c2..008e556c16 100644 --- a/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts +++ b/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts @@ -1,5 +1,5 @@ import { expect, it } from 'vitest' -import { BTreeIndex, innerJoin } from '../../src/index.js' +import { BTreeIndex, innerJoin } from '../../src/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { extractSimpleComparisons } from '../../src/query/expression-helpers.js' @@ -406,8 +406,11 @@ it.each([`resolve`, `reject`, `cleanup`] as const)( query: (q) => q .from({ left }) - .join(innerJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.group, rightRow.group))) + .join( + innerJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.group, rightRow.group), + ), + ) .select(({ left: leftRow, right: rightRow }) => ({ leftId: leftRow.id, rightId: rightRow.id, diff --git a/packages/db/tests/query/load-subset-subquery.test.ts b/packages/db/tests/query/load-subset-subquery.test.ts index 4189eae4ce..8976748430 100644 --- a/packages/db/tests/query/load-subset-subquery.test.ts +++ b/packages/db/tests/query/load-subset-subquery.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { and, +import { + and, coalesce, createLiveQueryCollection, eq, @@ -11,7 +12,8 @@ import { and, lt, lte, not, - or } from '../../src/query/index.js' + or, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { PropRef, Value } from '../../src/query/ir.js' import type { Collection } from '../../src/collection/index.js' @@ -286,8 +288,13 @@ describe(`loadSubset with subqueries`, () => { // Use subquery in main query return q .from({ charge: chargesCollection }) - .join(fullJoin({ prepaidOrder: prepaidOrderQ }, ({ charge, prepaidOrder }) => - eq(charge.address_id, prepaidOrder.address_id))) + .join( + fullJoin( + { prepaidOrder: prepaidOrderQ }, + ({ charge, prepaidOrder }) => + eq(charge.address_id, prepaidOrder.address_id), + ), + ) }) cleanups.push(ordersCollection, subqueryQuery) @@ -359,8 +366,13 @@ describe(`loadSubset with subqueries`, () => { // Use subquery in main query return q .from({ charge: chargesCollection }) - .join(fullJoin({ prepaidOrder: prepaidOrderQ }, ({ charge, prepaidOrder }) => - eq(charge.address_id, prepaidOrder.address_id))) + .join( + fullJoin( + { prepaidOrder: prepaidOrderQ }, + ({ charge, prepaidOrder }) => + eq(charge.address_id, prepaidOrder.address_id), + ), + ) }) cleanups.push(ordersCollection, subqueryQuery) @@ -404,8 +416,11 @@ describe(`loadSubset with subqueries`, () => { return q .from({ charge: chargesCollection }) - .join(fullJoin({ order: orderedOrders }, ({ charge, order }) => - eq(charge.address_id, order.address_id))) + .join( + fullJoin({ order: orderedOrders }, ({ charge, order }) => + eq(charge.address_id, order.address_id), + ), + ) }) cleanups.push(ordersCollection, query) diff --git a/packages/db/tests/query/optimizer-semantics-oracle.test.ts b/packages/db/tests/query/optimizer-semantics-oracle.test.ts index 391b13ebfb..6c66790837 100644 --- a/packages/db/tests/query/optimizer-semantics-oracle.test.ts +++ b/packages/db/tests/query/optimizer-semantics-oracle.test.ts @@ -1,7 +1,12 @@ import { describe, expect, test } from 'vitest' import { applyOptimizations } from '../../src/query/optimizer-joins.js' import { processJoins } from '../../src/query/compiler/joins.js' -import { createLiveQueryCollection, innerJoin, leftJoin, rightJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + innerJoin, + leftJoin, + rightJoin, +} from '../../src/query/index.js' /** * An outer predicate on an aggregate subquery's result must run after the * aggregate. Moving it to source rows can change or remove the result. @@ -142,7 +147,9 @@ describe('optimizer aggregate semantics', () => { query: (q) => q .from({ s: aggregate }) - .join(innerJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target))) + .join( + innerJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)), + ) .select(({ s }) => ({ total: s.total })), }) expect(materialized.toArray.map(stripVirtualProps)).toEqual( @@ -225,7 +232,9 @@ describe('optimizer aggregate semantics', () => { })) return q .from({ s: summary }) - .join(leftJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target))) + .join( + leftJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)), + ) .where(({ s }) => outerPredicate === `accept` ? eq(s.k, 2) : eq(s.k, 1), ) @@ -500,7 +509,9 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: join.type, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -564,7 +575,9 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -572,7 +585,9 @@ describe(`optimizer residual convergence`, () => { new PropRef([`member`, `teamId`]), ]), }, - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `inner`, from: new CollectionRef(collection, `tag`), on: new Func('eq', [ @@ -619,7 +634,9 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: joinType, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -651,7 +668,9 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -684,7 +703,9 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -713,7 +734,9 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -751,7 +774,9 @@ describe(`optimizer residual convergence`, () => { const query: QueryIR = { from: new CollectionRef(collection, `team`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -759,7 +784,9 @@ describe(`optimizer residual convergence`, () => { new PropRef([`member`, `teamId`]), ]), }, - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: laterJoin, from: new CollectionRef(collection, `tag`), on: new Func('eq', [ @@ -820,7 +847,9 @@ describe(`optimizer residual convergence`, () => { `team`, ), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -859,7 +888,9 @@ describe(`optimizer residual convergence`, () => { `team`, ), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -918,8 +949,11 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .join(leftJoin({ member: memberCollection }, ({ team, member }) => - eq(team.id, member.teamId))) + .join( + leftJoin({ member: memberCollection }, ({ team, member }) => + eq(team.id, member.teamId), + ), + ) .where(({ team }) => eq(team.active, true)) .where(() => new Value(false)), }) @@ -977,8 +1011,11 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .join(leftJoin({ member: memberCollection }, ({ team, member }) => - eq(team.id, member.teamId))) + .join( + leftJoin({ member: memberCollection }, ({ team, member }) => + eq(team.id, member.teamId), + ), + ) .where(({ team }) => eq(team.active, true)) .where(({ member }) => or(isUndefined(member.id), eq(member.userId, 100)), @@ -993,8 +1030,12 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .join(rightJoin({ member: memberCollection }, ({ team, member }) => - eq(team.id, member.teamId))) + .join( + rightJoin( + { member: memberCollection }, + ({ team, member }) => eq(team.id, member.teamId), + ), + ) .where(({ member }) => eq(member.userId, 100)) .where(({ team }) => or(isUndefined(team.id), eq(team.active, true)), @@ -1070,10 +1111,16 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .join(leftJoin({ member: memberCollection }, ({ team, member }) => - eq(team.id, member.teamId))) - .join(innerJoin({ tag: tagCollection }, ({ team, tag }) => - eq(team.id, tag.teamId))) + .join( + leftJoin({ member: memberCollection }, ({ team, member }) => + eq(team.id, member.teamId), + ), + ) + .join( + innerJoin({ tag: tagCollection }, ({ team, tag }) => + eq(team.id, tag.teamId), + ), + ) .where(({ member }) => or(isUndefined(member.id), eq(member.userId, 100)), ) diff --git a/packages/db/tests/query/optimizer.test.ts b/packages/db/tests/query/optimizer.test.ts index 02badefe29..1cde4243b6 100644 --- a/packages/db/tests/query/optimizer.test.ts +++ b/packages/db/tests/query/optimizer.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest' +import { grouping } from '../../src/query/compiler/grouping.js' import { applyOptimizations } from '../../src/query/optimizer-joins.js' import { processJoins } from '../../src/query/compiler/joins.js' import { optimizeQuery } from '../../src/query/optimizer.js' @@ -47,7 +48,7 @@ function createOr(...args: Array) { } function createAgg(name: string, ...args: Array) { - return new Aggregate(name, args) + return new Aggregate(name, args, grouping) } describe(`Query Optimizer`, () => { @@ -135,7 +136,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -166,7 +169,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -208,7 +213,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new QueryRef(distinctSource, `p`), type: `inner`, on: createEq(createPropRef(`u`, `id`), createPropRef(`p`, `id`)), @@ -229,7 +236,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -277,7 +286,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -316,7 +327,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -353,7 +366,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -390,7 +405,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -433,7 +450,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -493,7 +512,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -527,7 +548,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -568,7 +591,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -605,7 +630,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(existingSubquery, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -650,7 +677,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(deeplyNestedQuery, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -678,7 +707,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -703,7 +734,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -758,7 +791,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -781,7 +816,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -816,7 +853,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -859,7 +898,9 @@ describe(`Query Optimizer`, () => { `u`, ), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -907,7 +948,9 @@ describe(`Query Optimizer`, () => { `u`, ), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -963,7 +1006,9 @@ describe(`Query Optimizer`, () => { source.bindingId, ), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1000,7 +1045,9 @@ describe(`Query Optimizer`, () => { `u`, ), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new QueryRef( { from: new CollectionRef(mockCollection, `p`), @@ -1080,7 +1127,9 @@ describe(`Query Optimizer`, () => { `u`, ), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1113,7 +1162,9 @@ describe(`Query Optimizer`, () => { const simpleQuery: QueryIR = { from: new CollectionRef(mockCollection, `u`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1149,7 +1200,9 @@ describe(`Query Optimizer`, () => { // Add JOIN and WHERE to make it optimizable deepQuery.join = [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq(createPropRef(`u`, `id`), createPropRef(`p`, `user_id`)), @@ -1175,7 +1228,9 @@ describe(`Query Optimizer`, () => { `u`, ), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1231,7 +1286,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(sharedSubquery, `main_users`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new QueryRef(sharedSubquery, `other_users`), // Same subquery object! type: `inner`, on: createEq( @@ -1291,7 +1348,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1334,7 +1393,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithLimitedOrder, `top_users`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1371,7 +1432,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithSelect, `filtered_users`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1419,7 +1482,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithHaving, `dept_stats`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1454,7 +1519,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithFnSelect, `computed_users`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1502,7 +1569,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithOrderOnly, `sorted_users`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1554,7 +1623,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(safeSubquery, `users`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new QueryRef(unsafeSubquery, `dept_stats`), type: `inner`, on: createEq( @@ -1601,7 +1672,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1661,7 +1734,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1751,7 +1826,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(teamsCollection, `team`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `left`, from: new CollectionRef(teamMembersCollection, `teamMember`), on: createEq( @@ -1796,7 +1873,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(usersCollection, `user`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `right`, from: new CollectionRef(profilesCollection, `profile`), on: createEq( @@ -1846,7 +1925,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(ordersCollection, `order`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `full`, from: new CollectionRef(paymentsCollection, `payment`), on: createEq( @@ -1891,7 +1972,9 @@ describe(`Query Optimizer`, () => { const query: QueryIR = { from: new CollectionRef(usersCollection, `user`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, type: `inner`, from: new CollectionRef(departmentsCollection, `dept`), on: createEq( diff --git a/packages/db/tests/query/optional-fields-negative.test-d.ts b/packages/db/tests/query/optional-fields-negative.test-d.ts index a77313b820..8fcb6037a4 100644 --- a/packages/db/tests/query/optional-fields-negative.test-d.ts +++ b/packages/db/tests/query/optional-fields-negative.test-d.ts @@ -1,5 +1,11 @@ import { describe, expectTypeOf, test } from 'vitest' -import { createLiveQueryCollection, eq, gt, innerJoin, leftJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + gt, + innerJoin, + leftJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -84,11 +90,12 @@ describe(`Optional Fields - Type Safety Tests`, () => { const query = createLiveQueryCollection({ query: (q) => - q.from({ user: usersCollection }).join(innerJoin({ dept: departmentsCollection }, - ({ user, dept }) => { + q.from({ user: usersCollection }).join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => { // This should work correctly - department_id is optional but can be used in join return eq(user.department_id, dept.id) - })), + }), + ), }) expectTypeOf(query.toArray).toMatchTypeOf< @@ -159,8 +166,11 @@ describe(`Optional Fields - Type Safety Tests`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, diff --git a/packages/db/tests/query/optional-fields-runtime.test.ts b/packages/db/tests/query/optional-fields-runtime.test.ts index 4a42666773..f3fd3b082e 100644 --- a/packages/db/tests/query/optional-fields-runtime.test.ts +++ b/packages/db/tests/query/optional-fields-runtime.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest' -import { createLiveQueryCollection, eq, leftJoin } from '../../src/query/index.js' +import { createLiveQueryCollection, + eq, + groupKeys, + leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -159,8 +162,11 @@ describe(`Optional Fields - Runtime Tests`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, @@ -193,7 +199,7 @@ describe(`Optional Fields - Runtime Tests`, () => { query: (q) => q .from({ user: usersCollection }) - .groupBy(({ user }) => user.department_id) + .groupBy(groupKeys(({ user }) => user.department_id)) .select(({ user }) => ({ department_id: user.department_id, })), diff --git a/packages/db/tests/query/order-by.test.ts b/packages/db/tests/query/order-by.test.ts index cdee5fc597..ca3f2f88f2 100644 --- a/packages/db/tests/query/order-by.test.ts +++ b/packages/db/tests/query/order-by.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it } from 'vitest' import { leftJoin } from '@tanstack/db' +import { groupKeys } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' import { createLiveQueryCollection } from '../../src/query/live-query-collection.js' @@ -832,9 +833,13 @@ function createOrderByTests(autoIndex: `off` | `eager`): void { const collection = createLiveQueryCollection((q) => q .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .orderBy(({ departments }) => departments.name, `asc`) .orderBy(({ employees }) => employees.salary, `desc`) .select(({ employees, departments }) => ({ @@ -892,7 +897,7 @@ function createOrderByTests(autoIndex: `off` | `eager`): void { query: (qb) => qb .from({ vehicleDocuments: vehicleDocumentCollection }) - .groupBy((q) => q.vehicleDocuments.vin) + .groupBy(groupKeys((q) => q.vehicleDocuments.vin)) .orderBy((q) => q.vehicleDocuments.vin, `asc`) .select((q) => ({ vin: q.vehicleDocuments.vin, @@ -951,7 +956,7 @@ function createOrderByTests(autoIndex: `off` | `eager`): void { query: (qb) => qb .from({ vehicleDocuments: vehicleDocumentCollection }) - .groupBy((q) => q.vehicleDocuments.vin) + .groupBy(groupKeys((q) => q.vehicleDocuments.vin)) .orderBy((q) => max(q.vehicleDocuments.updatedAt), `desc`) .select((q) => ({ vin: q.vehicleDocuments.vin, @@ -1899,9 +1904,13 @@ function createOrderByTests(autoIndex: `off` | `eager`): void { const collection = createLiveQueryCollection((q) => q .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .orderBy(({ departments }) => departments.name, `asc`) .limit(5) .select(({ employees, departments }) => ({ @@ -1937,8 +1946,12 @@ function createOrderByTests(autoIndex: `off` | `eager`): void { const collection = createLiveQueryCollection((q) => q .from({ employee: employeesCollection }) - .join(leftJoin({ manager: employeesCollection }, ({ employee, manager }) => - eq(employee.id, manager.id))) + .join( + leftJoin( + { manager: employeesCollection }, + ({ employee, manager }) => eq(employee.id, manager.id), + ), + ) .orderBy(({ manager }) => manager.name, `asc`) .limit(3) .select(({ employee, manager }) => ({ diff --git a/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts b/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts index 8bba0da9a7..1ed7ab1c27 100644 --- a/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts +++ b/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts @@ -1,12 +1,15 @@ import { isDeepStrictEqual } from 'node:util' import { describe, expect, it } from 'vitest' import { fc, test as fcTest } from '@fast-check/vitest' -import { createLiveQueryCollection, eq, +import { + createLiveQueryCollection, + eq, innerJoin, isUndefined, leftJoin, not, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' @@ -1114,8 +1117,11 @@ describe(`synchronous initial settlement refinement`, () => { query: (q) => q .from({ row: rows }) - .join(leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId))) + .join( + leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId), + ), + ) .where(({ marker }) => not(isUndefined(marker.rowId))) .fn.where(({ row }) => row.rank > 0) .orderBy(({ row }) => row.rank) @@ -1217,8 +1223,11 @@ describe(`synchronous initial settlement refinement`, () => { query: (q) => q .from({ left }) - .join(leftJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.group, rightRow.group))) + .join( + leftJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.group, rightRow.group), + ), + ) .orderBy(({ left: leftRow }) => leftRow.rank) .limit(2) .select(({ left: leftRow, right: rightRow }) => ({ @@ -2017,8 +2026,11 @@ async function observeLaterFallback( query: (q) => q .from({ root: rootCollection }) - .join(leftJoin({ marker: markerCollection }, ({ root, marker }) => - eq(root.id, marker.rootId))) + .join( + leftJoin({ marker: markerCollection }, ({ root, marker }) => + eq(root.id, marker.rootId), + ), + ) .where(({ marker }) => not(isUndefined(marker.id))) .orderBy(({ root }) => root.at, { direction: `asc`, @@ -3068,7 +3080,9 @@ function createFullSourceQuery( switch (feature) { case `inner-join`: return base - .join(innerJoin({ tag: tags }, ({ row, tag }) => eq(row.tag, tag.tag))) + .join( + innerJoin({ tag: tags }, ({ row, tag }) => eq(row.tag, tag.tag)), + ) .orderBy(({ row }) => row.rank) .limit(2) .select(({ row }) => ({ id: row.id })) diff --git a/packages/db/tests/query/ordered-work-oracle.property.test.ts b/packages/db/tests/query/ordered-work-oracle.property.test.ts index 51f8448ece..dc14adf5c9 100644 --- a/packages/db/tests/query/ordered-work-oracle.property.test.ts +++ b/packages/db/tests/query/ordered-work-oracle.property.test.ts @@ -1,6 +1,11 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, it, vi } from 'vitest' -import { fullJoin as fullJoinOperator, innerJoin, leftJoin, toArray } from '../../src/query/index.js' +import { + fullJoin as fullJoinOperator, + innerJoin, + leftJoin, + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { localOnlyCollectionOptions } from '../../src/local-only.js' import { createDeferred } from '../../src/deferred.js' @@ -467,8 +472,11 @@ async function observeConsumer( const query = (q: InitialQueryBuilder) => { const joined = q .from({ row: source }) - .join(leftJoin({ marker: markerSource }, ({ row, marker }) => - eq(row.id, joinFilter === `to-one` ? marker.id : marker.rowId))) + .join( + leftJoin({ marker: markerSource }, ({ row, marker }) => + eq(row.id, joinFilter === `to-one` ? marker.id : marker.rowId), + ), + ) .where(({ row, marker }) => { switch (joinFilter) { case `joined-only`: @@ -1942,8 +1950,11 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .join(leftJoin({ marker }, ({ row, marker: child }) => - eq(row.id, child.rowId))) + .join( + leftJoin({ marker }, ({ row, marker: child }) => + eq(row.id, child.rowId), + ), + ) .orderBy(({ row }) => row.rank) .limit(1) .select(({ row }) => ({ @@ -2037,8 +2048,11 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .join(leftJoin({ marker: child }, ({ row, marker }) => - eq(row.id, marker.rowId))) + .join( + leftJoin({ marker: child }, ({ row, marker }) => + eq(row.id, marker.rowId), + ), + ) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) .limit(1) @@ -2105,8 +2119,11 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .join(leftJoin({ marker: joined }, ({ row, marker }) => - eq(row.id, marker.rowId))) + .join( + leftJoin({ marker: joined }, ({ row, marker }) => + eq(row.id, marker.rowId), + ), + ) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) .limit(0) @@ -2229,8 +2246,11 @@ describe(`ordered source work oracle`, () => { .from({ order: orders }) .where(({ order }) => gte(order.scheduledAt, `2024-01-12`)) .where(({ order }) => eq(order.status, `queued`)) - .join(innerJoin({ charge: charges }, ({ order, charge }) => - eq(order.addressId, charge.addressId))), + .join( + innerJoin({ charge: charges }, ({ order, charge }) => + eq(order.addressId, charge.addressId), + ), + ), ) try { @@ -2351,8 +2371,11 @@ describe(`ordered source work oracle`, () => { const query = (q: InitialQueryBuilder) => q .from({ row: source }) - .join(innerJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId))) + .join( + innerJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId), + ), + ) .orderBy(({ row }) => row.rank) .limit(0) const live = @@ -2598,9 +2621,13 @@ describe(`ordered source work oracle`, () => { const live = createLiveQueryCollection((q) => q .from({ primaryRow: primary }) - .join(innerJoin({ secondaryRow: secondary }, - ({ primaryRow, secondaryRow }) => - eq(primaryRow.joinKey, secondaryRow.joinKey))) + .join( + innerJoin( + { secondaryRow: secondary }, + ({ primaryRow, secondaryRow }) => + eq(primaryRow.joinKey, secondaryRow.joinKey), + ), + ) .orderBy(({ primaryRow }) => primaryRow.rank) .limit(2), ) @@ -2717,9 +2744,13 @@ describe(`ordered source work oracle`, () => { q .from({ primaryRow: primary }) .where(({ primaryRow }) => eq(primaryRow.id, id)) - .join(innerJoin({ secondaryRow: secondary }, - ({ primaryRow, secondaryRow }) => - eq(primaryRow.joinKey, secondaryRow.joinKey))) + .join( + innerJoin( + { secondaryRow: secondary }, + ({ primaryRow, secondaryRow }) => + eq(primaryRow.joinKey, secondaryRow.joinKey), + ), + ) .orderBy(({ primaryRow }) => primaryRow.rank) .limit(1), ) @@ -2871,8 +2902,11 @@ describe(`ordered source work oracle`, () => { const live = createLiveQueryCollection((q) => q .from({ row: primary }) - .join(leftJoin({ child: secondary }, ({ row, child }) => - eq(row.id, child.primaryId))) + .join( + leftJoin({ child: secondary }, ({ row, child }) => + eq(row.id, child.primaryId), + ), + ) .orderBy(({ row }) => row.rank) .limit(1) .select(({ row, child }) => ({ @@ -3123,8 +3157,11 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ root: rootCollection }) - .join(leftJoin({ child: childCollection }, ({ root, child }) => - eq(root.id, child.rootId))) + .join( + leftJoin({ child: childCollection }, ({ root, child }) => + eq(root.id, child.rootId), + ), + ) .where(({ child }) => not(isUndefined(child.rootId))) .orderBy(({ root }) => root.rank) .limit(1) @@ -3223,8 +3260,11 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ root: rootCollection }) - .join(leftJoin({ child: childCollection }, ({ root, child }) => - eq(root.id, child.rootId))) + .join( + leftJoin({ child: childCollection }, ({ root, child }) => + eq(root.id, child.rootId), + ), + ) .where(({ child }) => isUndefined(child.rootId)) .orderBy(({ root }) => root.rank) .limit(1) @@ -3396,8 +3436,11 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ root: rootCollection }) - .join(leftJoin({ child: childCollection }, ({ root, child }) => - eq(root.id, child.rootId))) + .join( + leftJoin({ child: childCollection }, ({ root, child }) => + eq(root.id, child.rootId), + ), + ) .where(({ child }) => isUndefined(child.rootId)) .orderBy(({ root }) => root.rank) .limit(1) @@ -4136,8 +4179,11 @@ describe(`ordered source work oracle`, () => { const query = (q: InitialQueryBuilder) => q .from({ row: root }) - .join(leftJoin({ marker: markerSource }, ({ row, marker }) => - eq(row.id, marker.rowId))) + .join( + leftJoin({ marker: markerSource }, ({ row, marker }) => + eq(row.id, marker.rowId), + ), + ) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) .limit(2) @@ -4248,8 +4294,11 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .join(leftJoin({ marker: joined }, ({ row, marker }) => - eq(row.id, marker.rowId))) + .join( + leftJoin({ marker: joined }, ({ row, marker }) => + eq(row.id, marker.rowId), + ), + ) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) .limit(1) @@ -5483,15 +5532,21 @@ async function runEagerWindow(testCase: EagerCase): Promise<{ ) => { const joinedRows = q .from({ row: source }) - .join(leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.link, marker.rowId))) + .join( + leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.link, marker.rowId), + ), + ) const ordered = ( fullJoin ? // Same aliases as the LEFT join; only the join kind differs. (q .from({ row: source }) - .join(fullJoinOperator({ marker: markers }, ({ row, marker }) => - eq(row.link, marker.rowId))) as unknown as typeof joinedRows) + .join( + fullJoinOperator({ marker: markers }, ({ row, marker }) => + eq(row.link, marker.rowId), + ), + ) as unknown as typeof joinedRows) : joined ? admitsWhenMatched ? joinedRows.where(({ marker }) => not(isUndefined(marker.rowId))) @@ -6546,8 +6601,11 @@ async function checkOptimisticWindow( .where(({ row }) => eq(row.eligible, true)) const left = q .from({ row: source }) - .join(leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.link, marker.rowId))) + .join( + leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.link, marker.rowId), + ), + ) const filtered = shape === `source` ? direct diff --git a/packages/db/tests/query/pagination-oracle.property.test.ts b/packages/db/tests/query/pagination-oracle.property.test.ts index ea7e410a29..0d5cc9de81 100644 --- a/packages/db/tests/query/pagination-oracle.property.test.ts +++ b/packages/db/tests/query/pagination-oracle.property.test.ts @@ -1,6 +1,6 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, it } from 'vitest' -import { BTreeIndex, innerJoin, leftJoin } from '../../src/index.js' +import { BTreeIndex, innerJoin, leftJoin } from '../../src/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { createLiveQueryCollection } from '../../src/query/live-query-collection.js' @@ -3006,8 +3006,11 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ parent: parentSource }) - .join(innerJoin({ child: childSource }, ({ parent, child }) => - eq(parent.groupId, child.groupId))) + .join( + innerJoin({ child: childSource }, ({ parent, child }) => + eq(parent.groupId, child.groupId), + ), + ) .orderBy(({ parent }) => parent.rank, `asc`) .orderBy(({ parent }) => parent.id, `asc`) .limit(2) @@ -3054,8 +3057,11 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ parent: parentSource }) - .join(innerJoin({ child: childSource }, ({ parent, child }) => - eq(parent.groupId, child.groupId))) + .join( + innerJoin({ child: childSource }, ({ parent, child }) => + eq(parent.groupId, child.groupId), + ), + ) .orderBy(({ parent }) => parent.rank, `asc`) .orderBy(({ parent }) => parent.id, `asc`) .limit(2) @@ -3102,8 +3108,11 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ parent: parentSource }) - .join(leftJoin({ child: childSource }, ({ parent, child }) => - eq(parent.childId, child.id))) + .join( + leftJoin({ child: childSource }, ({ parent, child }) => + eq(parent.childId, child.id), + ), + ) .orderBy(({ parent }) => parent.sourceRank, `asc`) .orderBy(({ child }) => child.score, `asc`) .orderBy(({ parent }) => parent.id, `asc`) @@ -6542,8 +6551,11 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ scope }) - .join(innerJoin({ row: source }, ({ scope: outer, row }) => - eq(outer.groupId, row.groupId))) + .join( + innerJoin({ row: source }, ({ scope: outer, row }) => + eq(outer.groupId, row.groupId), + ), + ) .where(({ row }) => eq(row.keep, true)) .orderBy(({ row }) => row.label, { direction: `asc`, diff --git a/packages/db/tests/query/query-api-type-algebra.test-d.ts b/packages/db/tests/query/query-api-type-algebra.test-d.ts index 0020d83181..7de11c71d3 100644 --- a/packages/db/tests/query/query-api-type-algebra.test-d.ts +++ b/packages/db/tests/query/query-api-type-algebra.test-d.ts @@ -13,7 +13,14 @@ import { describe, expectTypeOf, test } from 'vitest' * paired runtime test owns the unmatched-row value witness. Existing generic, * join, and nullable-leaf suites remain the broader compatibility owners. */ -import { Query, createLiveQueryCollection, eq, fullJoin, leftJoin, rightJoin } from '../../src/query/index.js' +import { + Query, + createLiveQueryCollection, + eq, + fullJoin, + leftJoin, + rightJoin, +} from '../../src/query/index.js' import type { Collection } from '../../src/collection/index.js' import type { Context, @@ -65,8 +72,11 @@ describe(`query API type algebra`, () => { const query = createLiveQueryCollection((q) => q .from({ row: rows }) - .join(leftJoin({ department: departments }, ({ row, department }) => - eq(row.departmentId, department.id))) + .join( + leftJoin({ department: departments }, ({ row, department }) => + eq(row.departmentId, department.id), + ), + ) .select(({ department }) => ({ department, departmentName: department.name, @@ -123,13 +133,19 @@ describe(`query API type algebra`, () => { ) { const joinedBranchA = new Query() .from({ rowA: rowsA }) - .join(leftJoin({ otherA: othersA }, ({ rowA, otherA }) => - eq(rowA.id, otherA.id))) + .join( + leftJoin({ otherA: othersA }, ({ rowA, otherA }) => + eq(rowA.id, otherA.id), + ), + ) .select(({ otherA }) => ({ other: otherA })) const joinedBranchB = new Query() .from({ rowB: rowsB }) - .join(leftJoin({ otherB: othersB }, ({ rowB, otherB }) => - eq(rowB.id, otherB.id))) + .join( + leftJoin({ otherB: othersB }, ({ rowB, otherB }) => + eq(rowB.id, otherB.id), + ), + ) .select(({ otherB }) => ({ other: otherB })) const joinedUnion = new Query() .unionAll(joinedBranchA, joinedBranchB) @@ -184,8 +200,11 @@ describe(`query API type algebra`, () => { const query = createLiveQueryCollection((q) => q .from({ row: rows }) - .join(leftJoin({ department: departments }, ({ row, department }) => - eq(row.departmentId, department.id))) + .join( + leftJoin({ department: departments }, ({ row, department }) => + eq(row.departmentId, department.id), + ), + ) .select(({ department }) => ({ ...department })), ) @@ -213,11 +232,13 @@ describe(`query API type algebra`, () => { const joined = new Query() .from({ row: rows }) - .join(leftJoin({ item: a }, ({ row, item }) => { - // @ts-expect-error Only the generic constraint is available. - void item.notGuaranteed - return eq(row.id, item.id) - })) + .join( + leftJoin({ item: a }, ({ row, item }) => { + // @ts-expect-error Only the generic constraint is available. + void item.notGuaranteed + return eq(row.id, item.id) + }), + ) .where(({ item }) => { // @ts-expect-error Nullable generic refs expose only guaranteed fields. void item.notGuaranteed @@ -269,13 +290,15 @@ describe(`query API type algebra`, () => { const joinedBranchUnion = new Query() .unionAll(aRows, bRows) - .join(leftJoin({ row: rows }, ({ id: itemId, row }) => { - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - // @ts-expect-error The joined source declares no other field. - void row.notGuaranteed - return eq(itemId, row.id) - })) + .join( + leftJoin({ row: rows }, ({ id: itemId, row }) => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + // @ts-expect-error The joined source declares no other field. + void row.notGuaranteed + return eq(itemId, row.id) + }), + ) .where((refs) => { void refs.id void refs.row.id @@ -289,7 +312,9 @@ describe(`query API type algebra`, () => { const rightJoinedBranchUnion = new Query() .unionAll(aRows, bRows) - .join(rightJoin({ row: rows }, ({ id: itemId, row }) => eq(itemId, row.id))) + .join( + rightJoin({ row: rows }, ({ id: itemId, row }) => eq(itemId, row.id)), + ) .select(({ id: itemId }) => ({ id: itemId })) return { diff --git a/packages/db/tests/query/query-api-type-algebra.test.ts b/packages/db/tests/query/query-api-type-algebra.test.ts index 9430eaf65b..49511b8279 100644 --- a/packages/db/tests/query/query-api-type-algebra.test.ts +++ b/packages/db/tests/query/query-api-type-algebra.test.ts @@ -1,5 +1,9 @@ import { expect, test } from 'vitest' -import { createLiveQueryCollection, eq, leftJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + leftJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -24,8 +28,11 @@ test(`an unmatched whole-object projection publishes undefined`, () => { query: (q) => q .from({ row: rows }) - .join(leftJoin({ department: departments }, ({ row, department }) => - eq(row.departmentId, department.id))) + .join( + leftJoin({ department: departments }, ({ row, department }) => + eq(row.departmentId, department.id), + ), + ) .select(({ department }) => ({ department })), }) @@ -68,13 +75,19 @@ test(`branch unions publish unmatched whole-object projections as undefined`, as const query = createLiveQueryCollection((q) => { const branchA = q .from({ rowA: rowsA }) - .join(leftJoin({ otherA: othersA }, ({ rowA, otherA }) => - eq(rowA.id, otherA.id))) + .join( + leftJoin({ otherA: othersA }, ({ rowA, otherA }) => + eq(rowA.id, otherA.id), + ), + ) .select(({ otherA }) => ({ other: otherA })) const branchB = q .from({ rowB: rowsB }) - .join(leftJoin({ otherB: othersB }, ({ rowB, otherB }) => - eq(rowB.id, otherB.id))) + .join( + leftJoin({ otherB: othersB }, ({ rowB, otherB }) => + eq(rowB.id, otherB.id), + ), + ) .select(({ otherB }) => ({ other: otherB })) return q.unionAll(branchA, branchB).select(({ other }) => ({ other })) diff --git a/packages/db/tests/query/query-once.test.ts b/packages/db/tests/query/query-once.test.ts index 12a5af1d30..e8a8270b9a 100644 --- a/packages/db/tests/query/query-once.test.ts +++ b/packages/db/tests/query/query-once.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { eq, innerJoin, queryOnce } from '../../src/query/index.js' +import { eq, innerJoin, queryOnce } from '../../src/query/index.js' import { CollectionImpl, createCollection } from '../../src/collection/index.js' import { Query } from '../../src/query/builder/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -233,8 +233,11 @@ describe(`queryOnce`, () => { const usersWithPosts = await queryOnce((q) => q .from({ user: usersCollection }) - .join(innerJoin({ post: postsCollection }, - ({ user, post }) => eq(user.id, post.authorId))) + .join( + innerJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.authorId), + ), + ) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, diff --git a/packages/db/tests/query/query-while-syncing.test.ts b/packages/db/tests/query/query-while-syncing.test.ts index 3d9017d428..bb41d6b311 100644 --- a/packages/db/tests/query/query-while-syncing.test.ts +++ b/packages/db/tests/query/query-while-syncing.test.ts @@ -1,5 +1,11 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { createLiveQueryCollection, eq, gt, innerJoin, leftJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + gt, + innerJoin, + leftJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createTransaction } from '../../src/transactions.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' @@ -317,8 +323,11 @@ describe(`Query while syncing`, () => { query: (q) => q .from({ user: usersCollection }) - .join(innerJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -441,8 +450,11 @@ describe(`Query while syncing`, () => { query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, @@ -572,10 +584,16 @@ describe(`Query while syncing`, () => { query: (q) => q .from({ c1: collection1 }) - .join(innerJoin({ c2: collection2 }, - ({ c1, c2 }) => eq(c1.id, c2.id))) - .join(innerJoin({ c3: collection3 }, - ({ c1, c3 }) => eq(c1.id, c3.id))) + .join( + innerJoin({ c2: collection2 }, ({ c1, c2 }) => + eq(c1.id, c2.id), + ), + ) + .join( + innerJoin({ c3: collection3 }, ({ c1, c3 }) => + eq(c1.id, c3.id), + ), + ) .select(({ c1 }) => ({ id: c1.id, name: c1.name, @@ -908,8 +926,11 @@ describe(`Query while syncing`, () => { query: (q) => q .from({ user: usersCollection }) - .join(innerJoin({ dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id))) + .join( + innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => ({ user_name: user.name, department_name: dept.name, diff --git a/packages/db/tests/query/scheduler.test.ts b/packages/db/tests/query/scheduler.test.ts index a872ce9753..c84031269d 100644 --- a/packages/db/tests/query/scheduler.test.ts +++ b/packages/db/tests/query/scheduler.test.ts @@ -1,5 +1,12 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { createLiveQueryCollection, eq, fullJoin, innerJoin, isNull, leftJoin } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + fullJoin, + innerJoin, + isNull, + leftJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { createTransaction } from '../../src/transactions.js' @@ -103,7 +110,11 @@ function setupLiveQueryCollections(id: string) { query: (q) => q .from({ user: users }) - .join(leftJoin({ task: tasks }, ({ user, task }) => eq(user.id, task.userId))) + .join( + leftJoin({ task: tasks }, ({ user, task }) => + eq(user.id, task.userId), + ), + ) .select(({ user, task }) => ({ userId: user.id, taskId: task.id, @@ -1081,8 +1092,11 @@ describe(`live query scheduler`, () => { ]) const query = new Query() .from({ left: leftQuery }) - .join(innerJoin({ right: rightQuery ?? rightSource }, - ({ left, right }) => eq(left.id, right.id))) + .join( + innerJoin({ right: rightQuery ?? rightSource }, ({ left, right }) => + eq(left.id, right.id), + ), + ) .select(({ left, right }) => ({ id: left.id, left: left.value, @@ -1201,8 +1215,11 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ left: liveQueryA }) - .join(fullJoin({ right: liveQueryB }, - ({ left, right }) => eq(left.id, right.id))) + .join( + fullJoin({ right: liveQueryB }, ({ left, right }) => + eq(left.id, right.id), + ), + ) .select(({ left, right }) => ({ left: left.value, right: right.value, @@ -1290,8 +1307,11 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ left: liveQueryA }) - .join(fullJoin({ right: collectionB }, - ({ left, right }) => eq(left.id, right.id))) + .join( + fullJoin({ right: collectionB }, ({ left, right }) => + eq(left.id, right.id), + ), + ) .select(({ left, right }) => ({ left: left.value, right: right.value, @@ -1375,8 +1395,11 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ left: liveQueryA }) - .join(fullJoin({ right: collectionB }, - ({ left, right }) => eq(left.id, right.id))) + .join( + fullJoin({ right: collectionB }, ({ left, right }) => + eq(left.id, right.id), + ), + ) .select(({ left, right }) => ({ left: left.value, right: right.value, @@ -1803,8 +1826,11 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ account: accounts }) - .join(leftJoin({ user: users }, ({ user, account }) => - eq(user.id, account.user_id))) + .join( + leftJoin({ user: users }, ({ user, account }) => + eq(user.id, account.user_id), + ), + ) .select(({ account, user }) => ({ account: account, profile: user.profile, @@ -1827,8 +1853,11 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ accountWithUser: accountsWithUsers }) - .join(leftJoin({ team: activeTeams }, ({ accountWithUser, team }) => - eq(team.team.account_id, accountWithUser.account.id))) + .join( + leftJoin({ team: activeTeams }, ({ accountWithUser, team }) => + eq(team.team.account_id, accountWithUser.account.id), + ), + ) .select(({ accountWithUser, team }) => ({ account: accountWithUser.account, profile: accountWithUser.profile, diff --git a/packages/db/tests/query/select-spread.test.ts b/packages/db/tests/query/select-spread.test.ts index dca3df766a..7854c6af29 100644 --- a/packages/db/tests/query/select-spread.test.ts +++ b/packages/db/tests/query/select-spread.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest' -import { createLiveQueryCollection, innerJoin } from '../../src/query/index.js' +import { createLiveQueryCollection, innerJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' import { add, eq, upper } from '../../src/query/builder/functions.js' diff --git a/packages/db/tests/query/subquery-user-value-oracle.test.ts b/packages/db/tests/query/subquery-user-value-oracle.test.ts index d59515b25b..d20ab77d9c 100644 --- a/packages/db/tests/query/subquery-user-value-oracle.test.ts +++ b/packages/db/tests/query/subquery-user-value-oracle.test.ts @@ -1,10 +1,12 @@ import { D2, MultiSet, output } from '@tanstack/db-ivm' import { describe, expect, it } from 'vitest' -import { count, +import { + count, createLiveQueryCollection, eq, innerJoin, - materialize } from '../../src/query/index.js' + materialize, +} from '../../src/query/index.js' import { buildQuery } from '../../src/query/builder/index.js' import { compileQuery } from '../../src/query/compiler/index.js' import { collectCollectionSources } from '../../src/query/ir.js' @@ -81,8 +83,11 @@ describe('subquery boundaries preserve operators and user rows', () => { .distinct() return q .from({ person: persons.collection }) - .join(innerJoin({ group: distinctGroups }, ({ person, group }) => - eq(person.groupId, group.groupId))) + .join( + innerJoin({ group: distinctGroups }, ({ person, group }) => + eq(person.groupId, group.groupId), + ), + ) .where(({ person }) => eq(person.enabled, true)) .select(({ person, group }) => ({ personId: person.id, @@ -174,8 +179,11 @@ describe('subquery boundaries preserve operators and user rows', () => { .distinct() return q .from({ person: persons.collection }) - .join(innerJoin({ group: distinctGroups }, ({ person, group }) => - eq(person.groupId, group.groupId))) + .join( + innerJoin({ group: distinctGroups }, ({ person, group }) => + eq(person.groupId, group.groupId), + ), + ) .where(({ person }) => eq(person.enabled, true)) .select(({ person, group }) => ({ personId: person.id, @@ -405,8 +413,11 @@ describe('subquery boundaries preserve operators and user rows', () => { .distinct() return q .from({ person: people.collection }) - .join(innerJoin({ group: groups }, ({ person, group }) => - eq(person.groupId, group.groupId))) + .join( + innerJoin({ group: groups }, ({ person, group }) => + eq(person.groupId, group.groupId), + ), + ) .where(({ group }) => eq(group.$key, 2)) .select(({ group }) => ({ groupId: group.groupId })) }, @@ -439,8 +450,11 @@ describe('subquery boundaries preserve operators and user rows', () => { })) return q .from({ person: people.collection }) - .join(innerJoin({ result: stats }, ({ person, result }) => - eq(person.id, result.stats.total))) + .join( + innerJoin({ result: stats }, ({ person, result }) => + eq(person.id, result.stats.total), + ), + ) .where(({ result }) => eq(result.$key, 'single_group')) .select(({ result }) => ({ total: result.stats.total })) }, @@ -476,8 +490,11 @@ describe('subquery boundaries preserve operators and user rows', () => { })) return q .from({ person: persons.collection }) - .join(innerJoin({ result: stats }, ({ person, result }) => - eq(person.id, result.stats.total))) + .join( + innerJoin({ result: stats }, ({ person, result }) => + eq(person.id, result.stats.total), + ), + ) .where(({ result }) => eq(result.$key, 'single_group')) .select(({ person, result }) => ({ personId: person.id, @@ -549,8 +566,11 @@ describe('subquery boundaries preserve operators and user rows', () => { .select(({ item }) => ({ total: count(item.id) })) return q .from({ person: persons.collection }) - .join(innerJoin({ result: stats }, ({ person, result }) => - eq(person.id, result.total))) + .join( + innerJoin({ result: stats }, ({ person, result }) => + eq(person.id, result.total), + ), + ) .where(({ result }) => eq(result.$key, 'single_group')) .select(({ person, result }) => ({ personId: person.id, @@ -616,8 +636,11 @@ describe('subquery boundaries preserve operators and user rows', () => { .findOne() return q .from({ anchor: anchors.collection }) - .join(innerJoin({ first: firstQuery }, ({ anchor, first: row }) => - eq(anchor.id, row.id))) + .join( + innerJoin({ first: firstQuery }, ({ anchor, first: row }) => + eq(anchor.id, row.id), + ), + ) .select(({ first: row }) => ({ id: row.id })) }, getKey: (row) => row.id, @@ -630,8 +653,12 @@ describe('subquery boundaries preserve operators and user rows', () => { .findOne() return q .from({ first: firstQuery }) - .join(innerJoin({ anchor: anchors.collection }, ({ first: row, anchor }) => - eq(row.id, anchor.id))) + .join( + innerJoin( + { anchor: anchors.collection }, + ({ first: row, anchor }) => eq(row.id, anchor.id), + ), + ) .select(({ first: row }) => ({ id: row.id })) }, getKey: (row) => row.id, @@ -690,8 +717,11 @@ describe('subquery boundaries preserve operators and user rows', () => { query: (q) => { const firstJoined = q .from({ person: persons.collection }) - .join(innerJoin({ tag: tagRows.collection }, ({ person, tag }) => - eq(person.groupId, tag.groupId))) + .join( + innerJoin({ tag: tagRows.collection }, ({ person, tag }) => + eq(person.groupId, tag.groupId), + ), + ) .orderBy(({ person }) => person.id, 'asc') .select(({ person, tag }) => ({ personId: person.id, @@ -700,8 +730,11 @@ describe('subquery boundaries preserve operators and user rows', () => { .findOne() return q .from({ anchor: anchors.collection }) - .join(innerJoin({ first: firstJoined }, ({ anchor, first }) => - eq(anchor.id, first.personId))) + .join( + innerJoin({ first: firstJoined }, ({ anchor, first }) => + eq(anchor.id, first.personId), + ), + ) .select(({ anchor, first }) => ({ anchorId: anchor.id, tagId: first.tagId, @@ -803,8 +836,11 @@ describe('subquery boundaries preserve operators and user rows', () => { q .from({ person: persons.collection }) .where(({ person }) => eq(person.scope, anchor.id)) - .join(innerJoin({ tag: tagRows.collection }, ({ person, tag }) => - eq(person.groupId, tag.groupId))) + .join( + innerJoin({ tag: tagRows.collection }, ({ person, tag }) => + eq(person.groupId, tag.groupId), + ), + ) .orderBy(({ person }) => person.id, 'asc') .select(({ person, tag }) => ({ personId: person.id, diff --git a/packages/db/tests/query/subset-error-matrix.test.ts b/packages/db/tests/query/subset-error-matrix.test.ts index 0c476725ac..307c050029 100644 --- a/packages/db/tests/query/subset-error-matrix.test.ts +++ b/packages/db/tests/query/subset-error-matrix.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest' -import { createEffect, createLiveQueryCollection, eq, leftJoin } from '../../src/index.js' +import { + createEffect, + createLiveQueryCollection, + eq, + leftJoin, +} from '../../src/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { getLoadSubsetDemandKey } from '../../src/query/ir-stable-identity.js' @@ -147,8 +152,11 @@ function startEffect( query: (q) => q .from({ item: primary }) - .join(leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId))), + .join( + leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId), + ), + ), ...callbacks, }) } @@ -175,8 +183,11 @@ function startLive( return createLiveQueryCollection((q) => q .from({ item: primary }) - .join(leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId))), + .join( + leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId), + ), + ), ) } return createLiveQueryCollection((q) => q.from({ item: primary })) @@ -429,8 +440,11 @@ describe(`loadSubset failure matrix`, () => { query: (q) => q .from({ item: parent }) - .join(leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId))), + .join( + leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId), + ), + ), onBatch: () => {}, onSourceError: (error) => sourceErrors.push(error), }) @@ -440,8 +454,11 @@ describe(`loadSubset failure matrix`, () => { ? createLiveQueryCollection((q) => q .from({ item: parent }) - .join(leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId))), + .join( + leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId), + ), + ), ) : undefined @@ -505,8 +522,11 @@ describe(`loadSubset failure matrix`, () => { const live = createLiveQueryCollection((q) => q .from({ item: parent }) - .join(leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId))), + .join( + leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId), + ), + ), ) const originalQueueMicrotask = globalThis.queueMicrotask const queuedMicrotasks: Array<() => void> = [] diff --git a/packages/db/tests/query/union-all.test-d.ts b/packages/db/tests/query/union-all.test-d.ts index 45e0a73690..b211129b6c 100644 --- a/packages/db/tests/query/union-all.test-d.ts +++ b/packages/db/tests/query/union-all.test-d.ts @@ -1,10 +1,15 @@ import { describe, expectTypeOf, test } from 'vitest' -import { coalesce, count, createLiveQueryCollection, eq, +import { + coalesce, + count, + createLiveQueryCollection, + eq, fullJoin, gt, innerJoin, leftJoin, - rightJoin } from '../../src/query/index.js' + rightJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -331,8 +336,9 @@ describe(`unionAll types`, () => { return q .unionAll(messageRows, toolCallRows) - .join(innerJoin({ user: users }, - ({ userId, user }) => eq(userId, user.id))) + .join( + innerJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)), + ) .select(({ kind, id, user }) => ({ kind, id, @@ -366,8 +372,11 @@ describe(`unionAll types`, () => { const collection = createLiveQueryCollection((q) => { const messagesWithUsers = q .from({ message: messages }) - .join(innerJoin({ user: users }, - ({ message, user }) => eq(message.userId, user.id))) + .join( + innerJoin({ user: users }, ({ message, user }) => + eq(message.userId, user.id), + ), + ) const toolCallRows = q.from({ toolCall: toolCalls }) return q.unionAll(messagesWithUsers, toolCallRows) @@ -394,8 +403,11 @@ describe(`unionAll types`, () => { const collection = createLiveQueryCollection((q) => { const messagesWithUsers = q .from({ message: messages }) - .join(innerJoin({ user: users }, - ({ message, user }) => eq(message.userId, user.id))) + .join( + innerJoin({ user: users }, ({ message, user }) => + eq(message.userId, user.id), + ), + ) .select(({ message, user }) => ({ id: message.id, timestamp: message.timestamp, @@ -439,8 +451,11 @@ describe(`unionAll types`, () => { message: messages, toolCall: toolCalls, }) - .join(leftJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id))), + .join( + leftJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), + ), + ), ) expectTypeOf(collection.toArray).toMatchTypeOf< @@ -472,8 +487,11 @@ describe(`unionAll types`, () => { message: messages, toolCall: toolCalls, }) - .join(rightJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id))), + .join( + rightJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), + ), + ), ) expectTypeOf(collection.toArray).toMatchTypeOf< @@ -498,8 +516,11 @@ describe(`unionAll types`, () => { message: messages, toolCall: toolCalls, }) - .join(fullJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id))), + .join( + fullJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), + ), + ), ) expectTypeOf(collection.toArray).toMatchTypeOf< @@ -548,7 +569,9 @@ describe(`unionAll types`, () => { return q .unionAll(messageRows, toolCallRows) - .join(rightJoin({ user: users }, ({ userId, user }) => eq(userId, user.id))) + .join( + rightJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)), + ) .select(({ kind, id, user }) => ({ kind, id, @@ -590,7 +613,9 @@ describe(`unionAll types`, () => { return q .unionAll(messageRows, toolCallRows) - .join(fullJoin({ user: users }, ({ userId, user }) => eq(userId, user.id))) + .join( + fullJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)), + ) .select(({ kind, id, user }) => ({ kind, id, diff --git a/packages/db/tests/query/union-all.test.ts b/packages/db/tests/query/union-all.test.ts index 388a075d4f..79066c4806 100644 --- a/packages/db/tests/query/union-all.test.ts +++ b/packages/db/tests/query/union-all.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' -import { caseWhen, coalesce, concat, count, +import { + caseWhen, + coalesce, + concat, + count, createLiveQueryCollection, eq, fullJoin, @@ -7,7 +11,8 @@ import { caseWhen, coalesce, concat, count, innerJoin, leftJoin, rightJoin, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { @@ -370,8 +375,9 @@ describe(`unionAll`, () => { return q .unionAll(messageRows, toolCallRows) - .join(innerJoin({ user: users }, - ({ userId, user }) => eq(userId, user.id))) + .join( + innerJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)), + ) .select(({ kind, id, user }) => ({ kind, id, @@ -424,8 +430,9 @@ describe(`unionAll`, () => { return q .unionAll(messageRows, toolCallRows) - .join(innerJoin({ user: users }, - ({ userId, user }) => eq(userId, user.id))) + .join( + innerJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)), + ) .select(({ kind, id, user }) => ({ kind, id, @@ -911,8 +918,11 @@ describe(`unionAll`, () => { const collection = createLiveQueryCollection((q) => { const visibleMessagesWithUsers = q .from({ message: messages }) - .join(innerJoin({ user: users }, - ({ message, user }) => eq(message.userId, user.id))) + .join( + innerJoin({ user: users }, ({ message, user }) => + eq(message.userId, user.id), + ), + ) .where(({ message }) => eq(message.kind, `visible`)) .select(({ message, user }) => ({ id: message.id, @@ -964,9 +974,11 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .join(innerJoin({ user: users }, - ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id))) + .join( + innerJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), + ), + ) .select(({ message, toolCall, user }) => ({ messageId: message.id, toolCallId: toolCall.id, @@ -998,8 +1010,11 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .join(leftJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id))) + .join( + leftJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), + ), + ) .select(({ message, toolCall, user }) => ({ eventId: coalesce(message.id, toolCall.id), userName: user.name, @@ -1033,8 +1048,11 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .join(rightJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id))) + .join( + rightJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), + ), + ) .orderBy(({ user }) => user.id), ) @@ -1072,8 +1090,11 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .join(fullJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id))) + .join( + fullJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), + ), + ) .orderBy(({ message, toolCall, user }) => coalesce(message.timestamp, toolCall.timestamp, user.id), ), @@ -1117,9 +1138,11 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .join(innerJoin({ user: users }, - ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id))) + .join( + innerJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), + ), + ) .select(({ message, toolCall, user }) => ({ id: coalesce(message.id, toolCall.id), userName: user.name, @@ -1160,8 +1183,11 @@ describe(`unionAll`, () => { return q .from({ user: users }) - .join(leftJoin({ event: events }, ({ user, event }) => - eq(user.id, event.userId))) + .join( + leftJoin({ event: events }, ({ user, event }) => + eq(user.id, event.userId), + ), + ) .select(({ user, event }) => ({ userId: user.id, eventTimestamp: event.timestamp, @@ -1212,8 +1238,11 @@ describe(`unionAll`, () => { return q .from({ user: users }) - .join(leftJoin({ event: events }, ({ user, event }) => - eq(user.name, event.label))) + .join( + leftJoin({ event: events }, ({ user, event }) => + eq(user.name, event.label), + ), + ) .select(({ user, event }) => ({ userId: user.id, label: event.label, @@ -1270,8 +1299,11 @@ describe(`unionAll`, () => { return q .from({ user: users }) - .join(leftJoin({ event: events }, ({ user, event }) => - eq(user.id, event.userId))) + .join( + leftJoin({ event: events }, ({ user, event }) => + eq(user.id, event.userId), + ), + ) .select(({ user, event }) => ({ userId: user.id, eventTimestamp: event.timestamp, @@ -1376,8 +1408,12 @@ describe(`unionAll`, () => { createLiveQueryCollection((q) => q .from({ message: messages }) - .join(leftJoin({ toolCall: toolCalls, user: users }, ({ message, toolCall }) => - eq(message.id, toolCall.id))), + .join( + leftJoin( + { toolCall: toolCalls, user: users }, + ({ message, toolCall }) => eq(message.id, toolCall.id), + ), + ), ), ).toThrow(OnlyOneSourceAllowedError) }) diff --git a/packages/db/tests/query/validate-aliases.test.ts b/packages/db/tests/query/validate-aliases.test.ts index 5ac8873997..2467f015c4 100644 --- a/packages/db/tests/query/validate-aliases.test.ts +++ b/packages/db/tests/query/validate-aliases.test.ts @@ -1,8 +1,11 @@ import { beforeEach, describe, expect, test } from 'vitest' -import { createLiveQueryCollection, eq, +import { + createLiveQueryCollection, + eq, innerJoin, leftJoin, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -63,8 +66,11 @@ describe(`Alias validation in subqueries`, () => { query: (q) => { const locksAgg = q .from({ lock: locksCollection }) - .join(leftJoin({ vote: votesCollection }, ({ lock, vote }) => - eq(lock._id, vote.lockId))) + .join( + leftJoin({ vote: votesCollection }, ({ lock, vote }) => + eq(lock._id, vote.lockId), + ), + ) .select(({ lock }) => ({ _id: lock._id, lockName: lock.name, @@ -72,8 +78,11 @@ describe(`Alias validation in subqueries`, () => { return q .from({ vote: votesCollection }) - .join(leftJoin({ lock: locksAgg }, ({ vote, lock }) => - eq(lock._id, vote.lockId))) + .join( + leftJoin({ lock: locksAgg }, ({ vote, lock }) => + eq(lock._id, vote.lockId), + ), + ) .select(({ vote, lock }) => ({ voteId: vote._id, lockName: lock.lockName, @@ -98,8 +107,11 @@ describe(`Alias validation in subqueries`, () => { .select(({ item }) => ({ id: item._id })) return q .from({ item: locksCollection }) - .join(innerJoin({ child: reusable }, ({ item, child }) => - eq(item._id, child.id))) + .join( + innerJoin({ child: reusable }, ({ item, child }) => + eq(item._id, child.id), + ), + ) .select(({ item }) => ({ id: item._id })) }, }) @@ -117,10 +129,16 @@ describe(`Alias validation in subqueries`, () => { query: (q) => q .from({ lock: locksCollection }) - .join(leftJoin({ vote: votesCollection }, ({ lock, vote }) => - eq(vote.lockId, lock._id))) - .join(leftJoin({ vote: votesCollection }, ({ lock, vote }) => - eq(vote.lockId, lock._id))) + .join( + leftJoin({ vote: votesCollection }, ({ lock, vote }) => + eq(vote.lockId, lock._id), + ), + ) + .join( + leftJoin({ vote: votesCollection }, ({ lock, vote }) => + eq(vote.lockId, lock._id), + ), + ) .select(({ lock }) => ({ _id: lock._id })), }) }).toThrow(/alias "vote" more than once/) @@ -158,8 +176,11 @@ describe(`Alias validation in subqueries`, () => { .from({ tool: votesCollection }) .select(({ tool }) => ({ rid: tool._id })), ) - .join(innerJoin({ anchor: locksCollection }, ({ rid, anchor }) => - eq(rid, anchor._id))) + .join( + innerJoin({ anchor: locksCollection }, ({ rid, anchor }) => + eq(rid, anchor._id), + ), + ) .select(({ rid, anchor }) => ({ rid, votes: toArray( @@ -205,8 +226,11 @@ describe(`Alias validation in subqueries`, () => { .from({ lock: locksCollection }) .select(({ lock }) => ({ id: lock._id })), ) - .join(innerJoin({ vote: votesCollection }, ({ id, vote }) => - eq(vote._id, id))) + .join( + innerJoin({ vote: votesCollection }, ({ id, vote }) => + eq(vote._id, id), + ), + ) .select(({ id, vote }) => ({ id, lockId: vote.lockId })), }) await live.preload() @@ -253,8 +277,12 @@ describe(`Alias validation in subqueries`, () => { query: (q) => { const locksAgg = q .from({ lock: locksCollection }) - .join(leftJoin({ v: votesCollection }, // Uses unique alias "v" instead of "vote" - ({ lock, v }) => eq(lock._id, v.lockId))) + .join( + leftJoin( + { v: votesCollection }, // Uses unique alias "v" instead of "vote" + ({ lock, v }) => eq(lock._id, v.lockId), + ), + ) .select(({ lock }) => ({ _id: lock._id, lockName: lock.name, @@ -262,8 +290,11 @@ describe(`Alias validation in subqueries`, () => { return q .from({ vote: votesCollection }) - .join(leftJoin({ lock: locksAgg }, ({ vote, lock }) => - eq(lock._id, vote.lockId))) + .join( + leftJoin({ lock: locksAgg }, ({ vote, lock }) => + eq(lock._id, vote.lockId), + ), + ) .select(({ vote, lock }) => ({ voteId: vote._id, lockName: lock.lockName, diff --git a/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts b/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts index 2681f7a307..76297b2fae 100644 --- a/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts +++ b/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts @@ -1,9 +1,11 @@ import { describe, expectTypeOf, test } from 'vitest' -import { createLiveQueryCollection, +import { + createLiveQueryCollection, eq, leftJoin, materialize, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' /** * # Which types represent virtual-field-bearing rows? * @@ -226,8 +228,11 @@ describe(`virtual row field type boundary`, () => { unmatchedRows: toArray( q .from({ child: rows }) - .join(leftJoin({ missing: rows }, ({ child, missing }) => - eq(child.id, missing.id))) + .join( + leftJoin({ missing: rows }, ({ child, missing }) => + eq(child.id, missing.id), + ), + ) .select(({ missing }) => missing), ), objects: toArray( diff --git a/packages/db/tests/query/virtual-row-fields-oracle.test.ts b/packages/db/tests/query/virtual-row-fields-oracle.test.ts index d77bd207be..cdab607e8f 100644 --- a/packages/db/tests/query/virtual-row-fields-oracle.test.ts +++ b/packages/db/tests/query/virtual-row-fields-oracle.test.ts @@ -1,9 +1,11 @@ import { describe, expect, test } from 'vitest' -import { createLiveQueryCollection, +import { + createLiveQueryCollection, eq, leftJoin, materialize, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' /** * # Which published values carry virtual row fields? * @@ -281,8 +283,11 @@ describe(`virtual row field runtime boundary`, () => { q .from({ child: rows }) .where(({ child }) => eq(child.id, row.id)) - .join(leftJoin({ missing: missingRows }, ({ child, missing }) => - eq(child.id, missing.id))) + .join( + leftJoin({ missing: missingRows }, ({ child, missing }) => + eq(child.id, missing.id), + ), + ) .select(({ missing }) => missing), ), objects: toArray( From 3473d7662aab1a76c0f3e1818a91a9c7da8885a9 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 15:56:04 -0600 Subject: [PATCH 04/32] feat(db): carry the include materializer on include values include(), toArray(), and materialize() carry the materializer and the Collection-facade adapter. A bare subquery in select() throws IncludeOperatorRequiredError; include(subquery) gives the Collection-valued include. Join operators carry the materializer for custom-key reduction. Co-authored-by: Isaac --- docs/errors.md | 10 + packages/db/mangle-cache.json | 4 +- packages/db/src/errors.ts | 10 + packages/db/src/query/builder/clone-query.ts | 1 + packages/db/src/query/builder/functions.ts | 74 +- .../db/src/query/builder/include-functions.ts | 82 ++ packages/db/src/query/builder/index.ts | 69 +- .../db/src/query/builder/join-operators.ts | 3 + packages/db/src/query/builder/types.ts | 9 +- packages/db/src/query/compiler/index.ts | 24 +- packages/db/src/query/index.ts | 3 +- packages/db/src/query/ir.ts | 6 + packages/db/src/query/live/ARCHITECTURE.md | 13 +- .../query/live/collection-config-builder.ts | 94 +- .../db/src/query/live/includes-runtime.ts | 16 + packages/db/src/query/optimizer-joins.ts | 2 +- packages/db/tests/conformance/suite.ts | 5 +- packages/db/tests/error-sample-arguments.ts | 1 + packages/db/tests/fixtures/error-codes.json | 3 +- .../db/tests/fixtures/error-messages.json | 3 + packages/db/tests/live-query-options.test.ts | 10 +- .../tests/live-query-order-only-move.test.ts | 13 +- .../aggregate-select-oracle.property.test.ts | 6 +- .../tests/query/bucket-facade-adapter.test.ts | 9 +- .../db/tests/query/builder/buildQuery.test.ts | 17 +- .../query/builder/callback-types.test-d.ts | 124 ++- .../query/builder/functional-variants.test.ts | 40 +- .../db/tests/query/builder/functions.test.ts | 2 +- packages/db/tests/query/builder/join.test.ts | 203 ++-- .../db/tests/query/builder/ref-proxy.test.ts | 6 +- .../tests/query/builder/subqueries.test-d.ts | 28 +- .../db/tests/query/builder/union-all.test.ts | 18 +- packages/db/tests/query/case-when.test-d.ts | 21 +- packages/db/tests/query/case-when.test.ts | 17 +- .../collection-ref-compatibility.test.ts | 4 + .../db/tests/query/compiler/basic.test.ts | 5 +- .../query/compiler/group-by-pipeline.test.ts | 6 +- .../db/tests/query/compiler/group-by.test.ts | 12 +- .../tests/query/compiler/lazy-demand.test.ts | 6 +- .../db/tests/query/compiler/select.test.ts | 6 +- .../tests/query/compiler/subqueries.test.ts | 21 +- .../query/compiler/subquery-caching.test.ts | 16 +- .../tests/query/functional-variants.test-d.ts | 9 +- packages/db/tests/query/group-by-work.test.ts | 12 +- .../identity-output-shape-oracle.test.ts | 2 + ...ncludes-collection-oracle.property.test.ts | 111 +- .../includes-context-transport-oracle.test.ts | 9 +- ...includes-functional-input-boundary.test.ts | 35 +- ...ludes-functional-projection-oracle.test.ts | 69 +- .../tests/query/includes-lazy-loading.test.ts | 23 +- .../query/includes-space-oracle-fixture.ts | 40 +- .../includes-work-counter-oracle.test.ts | 5 +- packages/db/tests/query/includes.test-d.ts | 91 +- packages/db/tests/query/includes.test.ts | 987 ++++++++++-------- .../query/ir-stable-identity-oracle.test.ts | 24 +- .../query/optimizer-semantics-oracle.test.ts | 12 + packages/db/tests/query/optimizer.test.ts | 42 + .../query/optional-fields-runtime.test.ts | 6 +- .../tests/query/public-container-copy.test.ts | 11 +- .../query/replay-failure-boundary.test.ts | 10 +- packages/db/tests/query/scheduler.test.ts | 23 +- packages/db/tests/query/union-all.test.ts | 61 +- 62 files changed, 1650 insertions(+), 954 deletions(-) create mode 100644 packages/db/src/query/builder/include-functions.ts create mode 100644 packages/db/src/query/live/includes-runtime.ts diff --git a/docs/errors.md b/docs/errors.md index f3d2a9d261..2a647cc9ad 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -990,6 +990,16 @@ Development builds show the full message. For example: setWindow() cannot change the limit of a findOne() query. ``` + + +## Error 247: `IncludeOperatorRequiredError` + +Development builds show the full message. For example: + +```text +A subquery in select() needs include(), toArray(), or materialize(). Wrap the issues field: issues: include(q.from({ i: issues }).where(...)). Import include from @tanstack/db. +``` + ## Other errors and log messages These are plain `Error`, `TypeError`, `RangeError`, or diff --git a/packages/db/mangle-cache.json b/packages/db/mangle-cache.json index acc9210bbf..f5e125474e 100644 --- a/packages/db/mangle-cache.json +++ b/packages/db/mangle-cache.json @@ -415,5 +415,7 @@ "cancelRepairRetry": "cG", "coverSource": "fN", "windowRequestGeneration": "av", - "latestWindowRequest": "gE" + "latestWindowRequest": "gE", + "includesRuntimeCache": "gT", + "findJoinClause": "gU" } diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index 99473f6454..aebd727674 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -610,6 +610,16 @@ export class InvalidSourceError extends QueryBuilderError { } } +export class IncludeOperatorRequiredError extends QueryBuilderError { + constructor(field: string) { + super( + devBuild() && process.env.NODE_ENV !== `production` + ? `A subquery in select() needs include(), toArray(), or materialize(). Wrap the ${field} field: ${field}: include(q.from({ i: issues }).where(...)). Import include from @tanstack/db.` + : codedMessage(247, { field }), + ) + } +} + export class JoinOperatorRequiredError extends QueryBuilderError { constructor(type: string) { super( diff --git a/packages/db/src/query/builder/clone-query.ts b/packages/db/src/query/builder/clone-query.ts index 750f8c5ba0..b29a0c42aa 100644 --- a/packages/db/src/query/builder/clone-query.ts +++ b/packages/db/src/query/builder/clone-query.ts @@ -138,6 +138,7 @@ function cloneSelectValueForPlacement( value.correlationField, value.childCorrelationField, value.fieldName, + value.runtime, value.parentFilters, value.parentProjection, value.materialization, diff --git a/packages/db/src/query/builder/functions.ts b/packages/db/src/query/builder/functions.ts index c313d90cfa..84cf96e487 100644 --- a/packages/db/src/query/builder/functions.ts +++ b/packages/db/src/query/builder/functions.ts @@ -4,15 +4,14 @@ import { grouping } from '../compiler/grouping.js' import { isRefProxy, toExpression } from './ref-proxy.js' import { registerWrapper } from './wrapper-identity.js' import type { BasicExpression } from '../ir' +import type { IncludesRuntime } from '../live/includes-runtime.js' import type { RefProxy } from './ref-proxy.js' -import type { SingleResult } from '../../types.js' import type { Context, - GetInlineResult, RefLeaf, StringifiableScalar, } from './types.js' -import type { QueryBuilder } from './index.js' +import type { BaseQueryBuilder, QueryBuilder } from './index.js' type StringRef = RefLeaf | RefLeaf | RefLeaf @@ -325,7 +324,7 @@ export function concat( : codedMessage(110), ) } - return new ConcatToArrayWrapper(toArrayArg.query) + return new ConcatToArrayWrapper(toArrayArg.query, toArrayArg.runtime) } return new Func( @@ -767,7 +766,10 @@ export class ToArrayWrapper<_T = unknown> { readonly __brand = `ToArrayWrapper` as const declare readonly _type: `toArray` declare readonly _result: _T - constructor(public readonly query: QueryBuilder) { + constructor( + public readonly query: QueryBuilder, + public readonly runtime: IncludesRuntime, + ) { registerWrapper(this, `toArray()`) } } @@ -776,7 +778,10 @@ export class ConcatToArrayWrapper<_T = unknown> { readonly __brand = `ConcatToArrayWrapper` as const declare readonly _type: `concatToArray` declare readonly _result: _T - constructor(public readonly query: QueryBuilder) { + constructor( + public readonly query: QueryBuilder, + public readonly runtime: IncludesRuntime, + ) { registerWrapper(this, `concat(toArray())`) } } @@ -798,17 +803,14 @@ export class MaterializeWrapper< declare readonly _type: `materialize` declare readonly _result: _T declare readonly _isSingle: _IsSingle - constructor(public readonly query: QueryBuilder) { + constructor( + public readonly query: QueryBuilder, + public readonly runtime: IncludesRuntime, + ) { registerWrapper(this, `materialize()`) } } -export function toArray( - query: QueryBuilder, -): ToArrayWrapper> { - return new ToArrayWrapper(query) -} - function caseWhenHasOnlyExpressionValues(args: Array): boolean { const valueIndexes = getCaseWhenValueIndexes(args.length) return valueIndexes.every((index) => isExpressionValue(args[index])) @@ -850,41 +852,13 @@ function isConditionValue(value: CaseWhenValue | undefined): boolean { return isExpressionValue(value) && !Array.isArray(value) } -/** - * Materialize an includes subquery into a plain value on the parent row. - * - * - For multi-row subqueries, the parent receives an `Array` snapshot - * (equivalent to `toArray()`). - * - For `findOne()` subqueries, the parent receives a single `T | undefined` - * value — `undefined` when no child matches. - * - * The snapshot updates reactively: parent rows re-emit when the underlying - * children change. - * - * @example - * ```ts - * // Multi-row: produces Array on each project - * select(({ p }) => ({ - * ...p, - * issues: materialize( - * q.from({ i: issues }).where(({ i }) => eq(i.projectId, p.id)), - * ), - * })) - * - * // Singleton: produces Author | undefined on each post - * select(({ p }) => ({ - * ...p, - * author: materialize( - * q.from({ a: authors }).where(({ a }) => eq(a.id, p.authorId)).findOne(), - * ), - * })) - * ``` - */ -export function materialize( - query: QueryBuilder, -): MaterializeWrapper< - GetInlineResult, - TContext extends SingleResult ? true : false -> { - return new MaterializeWrapper(query) +export class IncludeWrapper<_TContext extends Context = any> { + readonly __brand = `IncludeWrapper` as const + declare readonly _context: _TContext + constructor( + public readonly query: BaseQueryBuilder, + public readonly runtime: IncludesRuntime, + ) { + registerWrapper(this, `include()`) + } } diff --git a/packages/db/src/query/builder/include-functions.ts b/packages/db/src/query/builder/include-functions.ts new file mode 100644 index 0000000000..07e227812f --- /dev/null +++ b/packages/db/src/query/builder/include-functions.ts @@ -0,0 +1,82 @@ +import { includesRuntime } from '../live/includes-runtime.js' +import { + IncludeWrapper, + MaterializeWrapper, + ToArrayWrapper, +} from './functions.js' +import type { SingleResult } from '../../types.js' +import type { BaseQueryBuilder, QueryBuilder } from './index.js' +import type { Context, GetInlineResult } from './types.js' + +// The include functions carry the include materializer. They live apart from +// the expression functions, which core modules import, so only apps that +// include subqueries bundle it. + +export function toArray( + query: QueryBuilder, +): ToArrayWrapper> { + return new ToArrayWrapper(query, includesRuntime) +} + +/** + * Materialize an includes subquery into a plain value on the parent row. + * + * - For multi-row subqueries, the parent receives an `Array` snapshot + * (equivalent to `toArray()`). + * - For `findOne()` subqueries, the parent receives a single `T | undefined` + * value — `undefined` when no child matches. + * + * The snapshot updates reactively: parent rows re-emit when the underlying + * children change. + * + * @example + * ```ts + * // Multi-row: produces Array on each project + * select(({ p }) => ({ + * ...p, + * issues: materialize( + * q.from({ i: issues }).where(({ i }) => eq(i.projectId, p.id)), + * ), + * })) + * + * // Singleton: produces Author | undefined on each post + * select(({ p }) => ({ + * ...p, + * author: materialize( + * q.from({ a: authors }).where(({ a }) => eq(a.id, p.authorId)).findOne(), + * ), + * })) + * ``` + */ +export function materialize( + query: QueryBuilder, +): MaterializeWrapper< + GetInlineResult, + TContext extends SingleResult ? true : false +> { + return new MaterializeWrapper(query, includesRuntime) +} + +/** + * Includes a correlated subquery as a live child Collection on each parent + * row. Use `toArray()` or `materialize()` for inline values instead. + * + * @example + * ```ts + * select(({ p }) => ({ + * ...p, + * issues: include( + * q.from({ i: issues }).where(({ i }) => eq(i.projectId, p.id)), + * ), + * })) + * ``` + */ +export function include( + query: QueryBuilder, +): IncludeWrapper { + // Every QueryBuilder is a BaseQueryBuilder; the public type hides internals. + return new IncludeWrapper( + query as unknown as BaseQueryBuilder, + includesRuntime, + ) +} diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index 7b608815d6..efae624b6f 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -18,6 +18,7 @@ import { isExpressionLike, } from '../ir.js' import { + IncludeOperatorRequiredError, InvalidSourceError, InvalidSourceTypeError, InvalidWhereExpressionError, @@ -38,12 +39,14 @@ import { import { CaseWhenWrapper, ConcatToArrayWrapper, + IncludeWrapper, MaterializeWrapper, ToArrayWrapper, } from './functions.js' import type { CollectionResolver } from './clone-query.js' import type { JoinOperator, JoinType } from './join-operators.js' import type { GroupKeys } from './group-keys.js' +import type { IncludesRuntime } from '../live/includes-runtime.js' import type { SourceClauseContext } from '../../errors.js' import type { NamespacedRow, SingleResult } from '../../types.js' import type { @@ -375,6 +378,7 @@ export class BaseQueryBuilder { on: onExpression, compile: operator.compile, optimize: operator.optimize, + materialize: operator.materialize, } const existingJoins = this.query.join || [] @@ -1020,7 +1024,7 @@ function buildNestedSelect( ) : toExpr(obj) } - if (obj instanceof BaseQueryBuilder) { + if (obj instanceof BaseQueryBuilder || obj instanceof IncludeWrapper) { if (!fieldName) { throw new Error( devBuild() && process.env.NODE_ENV !== `production` @@ -1028,7 +1032,15 @@ function buildNestedSelect( : codedMessage(114), ) } - return buildIncludesSubquery(obj, fieldName, parentScope, `collection`) + if (obj instanceof BaseQueryBuilder) + throw new IncludeOperatorRequiredError(fieldName) + return buildIncludesSubquery( + obj.query, + fieldName, + parentScope, + `collection`, + obj.runtime, + ) } if (obj instanceof ToArrayWrapper) { if (!(obj.query instanceof BaseQueryBuilder)) { @@ -1045,7 +1057,13 @@ function buildNestedSelect( : codedMessage(116), ) } - return buildIncludesSubquery(obj.query, fieldName, parentScope, `array`) + return buildIncludesSubquery( + obj.query, + fieldName, + parentScope, + `array`, + obj.runtime, + ) } if (obj instanceof ConcatToArrayWrapper) { if (!(obj.query instanceof BaseQueryBuilder)) { @@ -1062,7 +1080,13 @@ function buildNestedSelect( : codedMessage(118), ) } - return buildIncludesSubquery(obj.query, fieldName, parentScope, `concat`) + return buildIncludesSubquery( + obj.query, + fieldName, + parentScope, + `concat`, + obj.runtime, + ) } if (obj instanceof CaseWhenWrapper) { return buildConditionalSelect(obj, parentScope, fieldName) @@ -1079,8 +1103,15 @@ function buildNestedSelect( out[`${base}__${++spreadIndex}`] = v continue } - if (v instanceof BaseQueryBuilder) { - out[k] = buildIncludesSubquery(v, k, parentScope, `collection`) + if (v instanceof BaseQueryBuilder) throw new IncludeOperatorRequiredError(k) + if (v instanceof IncludeWrapper) { + out[k] = buildIncludesSubquery( + v.query, + k, + parentScope, + `collection`, + v.runtime, + ) continue } if (v instanceof ToArrayWrapper) { @@ -1091,7 +1122,13 @@ function buildNestedSelect( : codedMessage(119), ) } - out[k] = buildIncludesSubquery(v.query, k, parentScope, `array`) + out[k] = buildIncludesSubquery( + v.query, + k, + parentScope, + `array`, + v.runtime, + ) continue } if (v instanceof ConcatToArrayWrapper) { @@ -1102,7 +1139,13 @@ function buildNestedSelect( : codedMessage(120), ) } - out[k] = buildIncludesSubquery(v.query, k, parentScope, `concat`) + out[k] = buildIncludesSubquery( + v.query, + k, + parentScope, + `concat`, + v.runtime, + ) continue } if (v instanceof MaterializeWrapper) { @@ -1117,7 +1160,13 @@ function buildNestedSelect( const materialization: IncludesMaterialization = childQuery.singleResult ? `singleton` : `array` - out[k] = buildIncludesSubquery(v.query, k, parentScope, materialization) + out[k] = buildIncludesSubquery( + v.query, + k, + parentScope, + materialization, + v.runtime, + ) continue } if (v instanceof CaseWhenWrapper) { @@ -1399,6 +1448,7 @@ function buildIncludesSubquery( fieldName: string, parentScope: ParentScope, materialization: IncludesMaterialization, + runtime: IncludesRuntime, ): IncludesSubquery { const sourceQuery = childBuilder._getQuery() if (reusesAncestorBinding(sourceQuery, parentScope.bindingIds)) { @@ -1596,6 +1646,7 @@ function buildIncludesSubquery( parentRef, childRef, fieldName, + runtime, parentFilters.length > 0 ? parentFilters : undefined, parentProjection, materialization, diff --git a/packages/db/src/query/builder/join-operators.ts b/packages/db/src/query/builder/join-operators.ts index c95f6ef8c4..e426e3ddf1 100644 --- a/packages/db/src/query/builder/join-operators.ts +++ b/packages/db/src/query/builder/join-operators.ts @@ -1,6 +1,7 @@ import { processJoins } from '../compiler/joins.js' import { validateJoinConditions } from '../join-conditions.js' import { applyOptimizations } from '../optimizer-joins.js' +import { materializeCompilation } from '../live/materialized-pipeline.js' import type { Context, JoinOnCallback, @@ -27,6 +28,7 @@ export interface JoinOperator< type: TJoinType compile: typeof processJoins optimize: typeof applyOptimizations + materialize: typeof materializeCompilation validate: typeof validateJoinConditions } @@ -42,6 +44,7 @@ function joinOperator(type: TJoinType) { type, compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, validate: validateJoinConditions, }) } diff --git a/packages/db/src/query/builder/types.ts b/packages/db/src/query/builder/types.ts index 41a89c1179..25baa01f70 100644 --- a/packages/db/src/query/builder/types.ts +++ b/packages/db/src/query/builder/types.ts @@ -17,6 +17,7 @@ import type { import type { CaseWhenWrapper, ConcatToArrayWrapper, + IncludeWrapper, MaterializeWrapper, ToArrayWrapper, } from './functions.js' @@ -273,7 +274,7 @@ type SelectValue = | ConcatToArrayWrapper // concat(toArray(...)) wrapped subquery | CaseWhenWrapper // conditional projection | MaterializeWrapper // materialize() wrapped subquery (Array or T | undefined) - | QueryBuilder // includes subquery (produces a child Collection) + | IncludeWrapper // include() wrapped subquery (produces a child Collection) // Recursive shape for select objects allowing nested projections type SelectShape = { [key: string]: SelectValue | SelectShape } @@ -338,7 +339,7 @@ export type ResultTypeFromSelectValue = readonly _result?: infer T } ? ResultTypeFromCaseWhen - : TSelectValue extends QueryBuilder + : TSelectValue extends IncludeWrapper ? Collection> : TSelectValue extends AnyRef ? ExtractRef @@ -438,8 +439,8 @@ export type ResultTypeFromSelect = readonly _result?: infer T } ? ResultTypeFromCaseWhen - : // includes subquery (bare QueryBuilder) — produces a child Collection - TSelectObject[K] extends QueryBuilder< + : // include() subquery — produces a child Collection + TSelectObject[K] extends IncludeWrapper< infer TChildContext > ? Collection> diff --git a/packages/db/src/query/compiler/index.ts b/packages/db/src/query/compiler/index.ts index b102efcc8c..6281efffb2 100644 --- a/packages/db/src/query/compiler/index.ts +++ b/packages/db/src/query/compiler/index.ts @@ -11,7 +11,6 @@ import { import { isPlainObject } from '../../utils/type-guards.js' import { getOrCreate } from '../../utils/get-or-create.js' import { optimizeQuery } from '../optimizer.js' -import { materializeCompilation } from '../live/materialized-pipeline.js' import { createParentContext, createValueIdentity, @@ -88,6 +87,7 @@ import { } from './route-metadata.js' import { processSelect } from './select.js' import type { ValueIdentity } from '../equality-value-identity.js' +import type { IncludesRuntime } from '../live/includes-runtime.js' import type { CollectionSubscription } from '../../collection/subscription.js' import type { OrderByOptimizationInfo } from './order-by.js' import type { @@ -322,6 +322,8 @@ export interface IncludesCompilationResult { materialization: IncludesMaterialization /** Internal field used to unwrap scalar child selects */ scalarField?: string + /** The materializer the include value brought */ + runtime: IncludesRuntime } /** @@ -929,6 +931,7 @@ function compileQueryInternal( parentProjection: subquery.parentProjection, materialization: subquery.materialization, scalarField: subquery.scalarField, + runtime: subquery.runtime, }) } if (parentHasTopK) deferredIncludes.push(compileInclude) @@ -995,15 +998,16 @@ function compileQueryInternal( ], ), ) as ResultStream - const materializedInput = materializeCompilation({ - pipeline: inputPipeline, - includes: includesResults, - valueIdentity, - collectionId: mainCollectionId, - sourceWhereClauses, - aliasToCollectionId, - aliasRemapping, - }) + const materializedInput = + includesResults[0]!.runtime.materializeCompilation({ + pipeline: inputPipeline, + includes: includesResults, + valueIdentity, + collectionId: mainCollectionId, + sourceWhereClauses, + aliasToCollectionId, + aliasRemapping, + }) pipeline = materializedInput.pipeline.pipe( map(([key, [value]]) => { const row = { ...value } diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index 719127fd65..f37f594dde 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -83,9 +83,8 @@ export { min, max, // Includes helpers - toArray, - materialize, } from './builder/functions.js' +export { include, materialize, toArray } from './builder/include-functions.js' // Ref proxy utilities export type { Ref } from './builder/types.js' diff --git a/packages/db/src/query/ir.ts b/packages/db/src/query/ir.ts index 7e0db52d44..64beb2e42a 100644 --- a/packages/db/src/query/ir.ts +++ b/packages/db/src/query/ir.ts @@ -12,6 +12,8 @@ import type { NamespacedRow } from '../types' import type { processJoins } from './compiler/joins.js' import type { Grouping } from './compiler/grouping.js' +import type { IncludesRuntime } from './live/includes-runtime.js' +import type { materializeCompilation } from './live/materialized-pipeline.js' import type { applyOptimizations } from './optimizer-joins.js' export interface QueryIR { @@ -59,6 +61,8 @@ export interface JoinClause { // so apps without joins never import it. compile: typeof processJoins optimize: typeof applyOptimizations + // Reduces public keys for a joined query with a custom key. + materialize: typeof materializeCompilation } export type Where = @@ -296,6 +300,8 @@ export class IncludesSubquery extends BaseExpression { public correlationField: PropRef, // Parent-side ref (e.g., project.id) public childCorrelationField: PropRef, // Child-side ref (e.g., issue.projectId) public fieldName: string, // Result field name (e.g., "issues") + // The include value brings the materializer that publishes it. + public runtime: IncludesRuntime, public parentFilters?: Array, // WHERE clauses referencing parent aliases (applied post-join) public parentProjection?: Array, // Parent field refs used anywhere in the child plan public materialization: IncludesMaterialization = `collection`, diff --git a/packages/db/src/query/live/ARCHITECTURE.md b/packages/db/src/query/live/ARCHITECTURE.md index 86eae32fb9..b13a92443c 100644 --- a/packages/db/src/query/live/ARCHITECTURE.md +++ b/packages/db/src/query/live/ARCHITECTURE.md @@ -106,6 +106,14 @@ operators and a few boundary adapters: | Start and release asynchronous demand | `packages/db/src/query/live/subset-demand-controller.ts`, `packages/db/src/collection/subscription.ts` | | Ordered provider loading and continuation | `packages/db/src/query/live/ordered-source-loader.ts` | +Operator values carry their compilers, so an app bundles only the operators it +imports. `include()`, `toArray()`, and `materialize()` carry the materializer +and the Collection-facade adapter (`includes-runtime.ts`); join operators carry +the join compiler, the optimizer's join pushdown, and the materializer for key +reduction; aggregates and `groupKeys()` carry the grouping compiler. A query +that uses an operator always holds the value that carries it, so compilation +stays synchronous. + Queries without includes keep the original compiled pipeline and do not pay for facade state. The one exception is a joined query with a custom public-key function: its possible duplicate contributors still pass through the keyed @@ -628,8 +636,9 @@ tie-breaker, normally the child public key. An order-only change is a bucket-value change for arrays, singletons, concatenation, and Collection layout. -A bare child query is a Collection-valued include. It exposes one stable public -Collection facade per active bucket in that edge: +`include(childQuery)` is a Collection-valued include. A bare child query in a +select throws `IncludeOperatorRequiredError`. The include exposes one stable +public Collection facade per active bucket in that edge: ```text ActiveBucket + BucketRow -> ActiveBucketRow -> BucketFacade(bucket, Collection) diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index a8bf06e336..e9b3dea401 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -17,8 +17,6 @@ import { codedMessage, devBuild } from '../../error-message.js' import { CollectionSubscriber } from './collection-subscriber.js' import { getCollectionBuilder } from './collection-registry.js' import { LIVE_QUERY_INTERNAL } from './internal.js' -import { materializeCompilation } from './materialized-pipeline.js' -import { BucketFacadeAdapter } from './bucket-facade-adapter.js' import { scheduleQueryGraphRun } from './graph-scheduler.js' import { hasPendingJoinedWork as hasPendingJoinedSourceWork } from './subset-demand-controller.js' import { @@ -29,6 +27,10 @@ import { sourcePermitsInitialReadiness, } from './utils.js' import type { LiveQueryInternalUtils } from './internal.js' +import type { IncludesRuntime } from './includes-runtime.js' +import type { BasicExpression, JoinClause, QueryIR } from '../ir.js' +import type { BucketFacadeAdapter } from './bucket-facade-adapter.js' +import type { materializeCompilation } from './materialized-pipeline.js' import type { WindowOptions } from '../compiler/index.js' import type { SchedulerContextId } from '../../scheduler.js' import type { CollectionSubscription } from '../../collection/subscription.js' @@ -44,7 +46,6 @@ import type { UtilsRecord, } from '../../types.js' import type { Context, GetResult } from '../builder/types.js' -import type { BasicExpression, QueryIR } from '../ir.js' import type { LazyCollectionCallbacks } from '../compiler/joins.js' import type { Deferred } from '../../deferred.js' import type { @@ -156,6 +157,7 @@ export class CollectionConfigBuilder< private pipelineCache: ResultStream | undefined public sourceWhereClausesCache: Map> | undefined + private includesRuntimeCache: IncludesRuntime | undefined private bucketFacadesCache: ReturnType[`facades`] | undefined @@ -238,34 +240,30 @@ export class CollectionConfigBuilder< } /** - * Recursively checks if a query or any of its subqueries contains joins + * Returns a join clause from the query or any of its subqueries */ - private hasJoins(query: QueryIR): boolean { - // Check if this query has joins - if (query.join && query.join.length > 0) { - return true - } - - // Recursively check subqueries in the from clause - if (query.from.type === `queryRef`) { - if (this.hasJoins(query.from.query)) { - return true - } - } else if (query.from.type === `unionFrom`) { - for (const source of query.from.sources) { - if (source.type === `queryRef` && this.hasJoins(source.query)) { - return true - } - } - } else if (query.from.type === `unionAll`) { - for (const branch of query.from.queries) { - if (this.hasJoins(branch)) { - return true - } - } + private findJoinClause(query: QueryIR): JoinClause | undefined { + if (query.join && query.join.length > 0) return query.join[0] + const from = query.from + const nested = + from.type === `queryRef` + ? [from.query] + : from.type === `unionFrom` + ? from.sources.flatMap((source) => + source.type === `queryRef` ? [source.query] : [], + ) + : from.type === `unionAll` + ? from.queries + : [] + for (const subquery of nested) { + const joinClause = this.findJoinClause(subquery) + if (joinClause) return joinClause } + return undefined + } - return false + private hasJoins(query: QueryIR): boolean { + return this.findJoinClause(query) !== undefined } getConfig(): CollectionConfigSingleRowOption & { @@ -860,6 +858,7 @@ export class CollectionConfigBuilder< this.pipelineCache = undefined this.sourceWhereClausesCache = undefined this.bucketFacadesCache = undefined + this.includesRuntimeCache = undefined // Reset lazy source alias state this.lazySources.clear() @@ -910,14 +909,21 @@ export class CollectionConfigBuilder< }, ) - const materialized = materializeCompilation( - compilation, - this.config.getKey, - this.hasJoins(this.query), - ) + // Includes bring the materializer. A joined query with a custom key + // also reduces its public keys, so its join operator brings it too. + const includesRuntime = compilation.includes?.[0]?.runtime + const materialize = + includesRuntime?.materializeCompilation ?? + (this.config.getKey + ? this.findJoinClause(this.query)?.materialize + : undefined) + const materialized = materialize + ? materialize(compilation, this.config.getKey, this.hasJoins(this.query)) + : { pipeline: compilation.pipeline, facades: undefined } this.pipelineCache = materialized.pipeline this.sourceWhereClausesCache = compilation.sourceWhereClauses this.bucketFacadesCache = materialized.facades + this.includesRuntimeCache = includesRuntime const missingSources = this.collectionSources .map((source) => source.sourceId) @@ -962,14 +968,22 @@ export class CollectionConfigBuilder< }), ) - // A query whose pipeline was not materialized publishes its rows directly - // and pays for no facade state. + // A query without includes publishes its rows directly and pays for no + // facade state. With includes, the adapter also strips private route + // state at publication, even when every include is inline. A joined + // query with a custom key is materialized for key reduction only. const facadeCompilations = this.bucketFacadesCache - const bucketFacades = facadeCompilations - ? new BucketFacadeAdapter(this.id, facadeCompilations, (count) => { - syncState.messagesCount += count - }) - : undefined + const includesRuntime = this.includesRuntimeCache + const bucketFacades = + facadeCompilations && includesRuntime + ? includesRuntime.createBucketFacades( + this.id, + facadeCompilations, + (count) => { + syncState.messagesCount += count + }, + ) + : undefined if (bucketFacades) { syncState.unsubscribeCallbacks.add(() => bucketFacades.cleanup()) } diff --git a/packages/db/src/query/live/includes-runtime.ts b/packages/db/src/query/live/includes-runtime.ts new file mode 100644 index 0000000000..526a1a0726 --- /dev/null +++ b/packages/db/src/query/live/includes-runtime.ts @@ -0,0 +1,16 @@ +import { materializeCompilation } from './materialized-pipeline.js' +import { BucketFacadeAdapter } from './bucket-facade-adapter.js' + +/** + * The include materializer. `include()`, `toArray()`, and `materialize()` + * carry it onto the query IR, and join operators carry it for joined queries + * with a custom key, so other apps do not bundle it. + */ +export const includesRuntime = { + materializeCompilation, + createBucketFacades: ( + ...args: ConstructorParameters + ) => new BucketFacadeAdapter(...args), +} + +export type IncludesRuntime = typeof includesRuntime diff --git a/packages/db/src/query/optimizer-joins.ts b/packages/db/src/query/optimizer-joins.ts index 848667f92e..d3ed86b16c 100644 --- a/packages/db/src/query/optimizer-joins.ts +++ b/packages/db/src/query/optimizer-joins.ts @@ -154,6 +154,7 @@ function deepCopyQuery(query: QueryIR): QueryIR { on: joinClause.on, compile: joinClause.compile, optimize: joinClause.optimize, + materialize: joinClause.materialize, from: deepCopyFrom(joinClause.from) as CollectionSourceRef | QueryRefClass, })) @@ -495,4 +496,3 @@ function deepCopyFrom(from: From): From { ), ) } - diff --git a/packages/db/tests/conformance/suite.ts b/packages/db/tests/conformance/suite.ts index f09ca205fc..2b6bf40d3f 100644 --- a/packages/db/tests/conformance/suite.ts +++ b/packages/db/tests/conformance/suite.ts @@ -38,6 +38,7 @@ import { describe, expect, it } from 'vitest' * inputs, error status, and order-only moves. */ import { leftJoin, waitForPersistedInitialRender } from '@tanstack/db' +import { include } from '../../src/query/index.js' import { ScenarioLifetime } from './scenario-lifetime' import { ScenarioSources } from './scenario-sources' import { scenarioRegistry } from './registration' @@ -732,10 +733,10 @@ export function runSuite(rawDriver: LiveQueryDriver) { q.from({ persons: people.collection }).select(({ persons }: any) => ({ id: persons.id, name: persons.name, - issues: q + issues: include(q .from({ issues: issues.collection }) .where(({ issues: i }: any) => ops.eq(i.userId, persons.id)) - .select(({ issues: i }: any) => ({ id: i.id, title: i.title })), + .select(({ issues: i }: any) => ({ id: i.id, title: i.title }))), })), ) await h.flush() diff --git a/packages/db/tests/error-sample-arguments.ts b/packages/db/tests/error-sample-arguments.ts index eca05f00cc..16811d6b41 100644 --- a/packages/db/tests/error-sample-arguments.ts +++ b/packages/db/tests/error-sample-arguments.ts @@ -92,6 +92,7 @@ export const errorSampleArguments: Record>> = { ['unionAll clause', 'undefined'], ], JoinOperatorRequiredError: [['object']], + IncludeOperatorRequiredError: [['issues']], GroupingOperatorRequiredError: [[]], JoinConditionMustBeEqualityError: [[]], QueryMustHaveFromClauseError: [[]], diff --git a/packages/db/tests/fixtures/error-codes.json b/packages/db/tests/fixtures/error-codes.json index cd66de43e9..6fa0a66754 100644 --- a/packages/db/tests/fixtures/error-codes.json +++ b/packages/db/tests/fixtures/error-codes.json @@ -92,5 +92,6 @@ "DuplicateTransactionIdError": 233, "SetWindowFindOneLimitError": 244, "JoinOperatorRequiredError": 245, - "GroupingOperatorRequiredError": 246 + "GroupingOperatorRequiredError": 246, + "IncludeOperatorRequiredError": 247 } diff --git a/packages/db/tests/fixtures/error-messages.json b/packages/db/tests/fixtures/error-messages.json index f4b04f0e3b..3c10d78d33 100644 --- a/packages/db/tests/fixtures/error-messages.json +++ b/packages/db/tests/fixtures/error-messages.json @@ -266,5 +266,8 @@ ], "GroupingOperatorRequiredError": [ "groupBy() without an aggregate needs groupKeys(), such as .groupBy(groupKeys(({ todo }) => todo.status)). Import groupKeys from @tanstack/db. A query with an aggregate such as count() needs nothing extra." + ], + "IncludeOperatorRequiredError": [ + "A subquery in select() needs include(), toArray(), or materialize(). Wrap the issues field: issues: include(q.from({ i: issues }).where(...)). Import include from @tanstack/db." ] } diff --git a/packages/db/tests/live-query-options.test.ts b/packages/db/tests/live-query-options.test.ts index 9e3971f0fa..587ab0d23d 100644 --- a/packages/db/tests/live-query-options.test.ts +++ b/packages/db/tests/live-query-options.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { leftJoin } from '@tanstack/db' +import { include } from '../src/query/index.js' import { createCollection } from '../src/collection/index.js' import { DbClient, collectionOptions } from '../src/client.js' import { @@ -13,9 +14,8 @@ import { BaseQueryBuilder, getQueryIR } from '../src/query/builder/index.js' import { caseWhen, eq, - materialize, - toArray, } from '../src/query/builder/functions.js' +import { materialize, toArray } from '../src/query/builder/include-functions.js' import { collectSourceRefs } from '../src/query/ir.js' import type { DeferredLiveQueryCollections } from '../src/live-query-options.js' @@ -314,13 +314,13 @@ describe(`live query preparation`, () => { .where(({ child }) => eq(child.parentId, parent.id)) return { id: parent.id, - children: childQuery, + children: include(childQuery), nested: { inlineChildren: toArray(childQuery) }, materializedChildren: materialize(childQuery), conditionalChildren: caseWhen( eq(parent.id, `parent`), - childQuery, - childQuery, + include(childQuery), + include(childQuery), ), } }) diff --git a/packages/db/tests/live-query-order-only-move.test.ts b/packages/db/tests/live-query-order-only-move.test.ts index 5d6bf5181d..da333ea550 100644 --- a/packages/db/tests/live-query-order-only-move.test.ts +++ b/packages/db/tests/live-query-order-only-move.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { include } from '../src/query/index.js' import { createCollection } from '../src/collection/index.js' import { createDeferred } from '../src/deferred.js' import { createLiveQueryCollection } from '../src/query/live-query-collection.js' @@ -392,11 +393,11 @@ describe(`order-only move (RFC #1623 phase 4)`, () => { const lq = createLiveQueryCollection((q) => q.from({ parent: parents }).select(({ parent }) => ({ id: parent.id, - children: q + children: include(q .from({ child: children }) .where(({ child }) => eq(child.parentId, parent.id)) .orderBy(({ child }) => child.position) - .select(({ child }) => ({ id: child.id, name: child.name })), + .select(({ child }) => ({ id: child.id, name: child.name }))), })), ) await lq.preload() @@ -463,18 +464,18 @@ describe(`order-only move (RFC #1623 phase 4)`, () => { const lq = createLiveQueryCollection((q) => q.from({ org: orgs }).select(({ org }) => ({ id: org.id, - teams: q + teams: include(q .from({ team: teams }) .where(({ team }) => eq(team.orgId, org.id)) .orderBy(({ team }) => team.position) .select(({ team }) => ({ id: team.id, - members: q + members: include(q .from({ member: members }) .where(({ member }) => eq(member.teamId, team.id)) .orderBy(({ member }) => member.position) - .select(({ member }) => ({ id: member.id, name: member.name })), - })), + .select(({ member }) => ({ id: member.id, name: member.name }))), + }))), })), ) await lq.preload() diff --git a/packages/db/tests/query/aggregate-select-oracle.property.test.ts b/packages/db/tests/query/aggregate-select-oracle.property.test.ts index 3dbd641e60..5df20e5805 100644 --- a/packages/db/tests/query/aggregate-select-oracle.property.test.ts +++ b/packages/db/tests/query/aggregate-select-oracle.property.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { fc } from '@fast-check/vitest' -import { add, +import { + add, caseWhen, coalesce, count, @@ -11,7 +12,8 @@ import { add, max, sum, toArray, - upper } from '../../src/query/index.js' + upper, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { AggregateFunctionNotInSelectError, diff --git a/packages/db/tests/query/bucket-facade-adapter.test.ts b/packages/db/tests/query/bucket-facade-adapter.test.ts index 5c1564869e..eda6a2f0f6 100644 --- a/packages/db/tests/query/bucket-facade-adapter.test.ts +++ b/packages/db/tests/query/bucket-facade-adapter.test.ts @@ -1,5 +1,6 @@ import { D2, MultiSet } from '@tanstack/db-ivm' import { describe, expect, it, vi } from 'vitest' +import { include } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createLiveQueryCollection } from '../../src/query/live-query-collection.js' import { eq } from '../../src/query/builder/functions.js' @@ -630,9 +631,11 @@ describe(`BucketFacadeAdapter`, () => { const live = createLiveQueryCollection((q) => q.from({ parent: parents }).select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)), + ), })), ) diff --git a/packages/db/tests/query/builder/buildQuery.test.ts b/packages/db/tests/query/builder/buildQuery.test.ts index 1f13e41742..983296bc8d 100644 --- a/packages/db/tests/query/builder/buildQuery.test.ts +++ b/packages/db/tests/query/builder/buildQuery.test.ts @@ -61,9 +61,13 @@ describe(`buildQuery function`, () => { const query = buildQuery((q) => q .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .select(({ employees, departments }) => ({ employee_name: employees.name, department_name: departments.name, @@ -120,8 +124,11 @@ describe(`buildQuery function`, () => { const query = buildQuery((q) => q .from({ comment: commentsCollection }) - .join(leftJoin({ user: usersCollection }, ({ comment, user }) => - eq(comment.user_id, user.id))) + .join( + leftJoin({ user: usersCollection }, ({ comment, user }) => + eq(comment.user_id, user.id), + ), + ) .where(({ comment }) => or(eq(comment.id, 1), eq(comment.id, 2))) .orderBy(({ comment }) => comment.date, `desc`) .select(({ comment, user }) => ({ diff --git a/packages/db/tests/query/builder/callback-types.test-d.ts b/packages/db/tests/query/builder/callback-types.test-d.ts index 45276047eb..3fe061c8a5 100644 --- a/packages/db/tests/query/builder/callback-types.test-d.ts +++ b/packages/db/tests/query/builder/callback-types.test-d.ts @@ -115,8 +115,11 @@ describe(`Query Builder Callback Types`, () => { test(`refProxy with joins in select callback`, () => { new Query() .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .select(({ user, dept }) => { // Test cross-table property access expectTypeOf( @@ -268,8 +271,11 @@ describe(`Query Builder Callback Types`, () => { test(`refProxy with joins in where callback`, () => { new Query() .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .where(({ user, dept }) => { expectTypeOf(user.active).toEqualTypeOf>() expectTypeOf(dept.active).toEqualTypeOf>() @@ -286,9 +292,8 @@ describe(`Query Builder Callback Types`, () => { describe(`JOIN callback types`, () => { test(`refProxy types in join on callback`, () => { - new Query() - .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { + new Query().from({ user: usersCollection }).join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { // Test property access for join conditions expectTypeOf( user.department_id, @@ -296,38 +301,49 @@ describe(`Query Builder Callback Types`, () => { expectTypeOf(dept.id).toEqualTypeOf>() return eq(user.department_id, dept.id) - })) + }), + ) }) test(`complex join conditions`, () => { - new Query() - .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { + new Query().from({ user: usersCollection }).join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { // Test complex join conditions with multiple operators expectTypeOf( and(eq(user.department_id, dept.id), eq(dept.active, true)), ).toEqualTypeOf>() return and(eq(user.department_id, dept.id), eq(dept.active, true)) - })) + }), + ) }) test(`multiple joins with correct context`, () => { new Query() .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) - .join(leftJoin({ project: projectsCollection }, ({ user, dept, project }) => { - expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept.id).toEqualTypeOf>() - expectTypeOf(project.user_id).toEqualTypeOf>() - expectTypeOf(project.department_id).toEqualTypeOf>() - - return and( - eq(project.user_id, user.id), - eq(project.department_id, dept.id), - ) - })) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) + .join( + leftJoin( + { project: projectsCollection }, + ({ user, dept, project }) => { + expectTypeOf(user.id).toEqualTypeOf>() + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(project.user_id).toEqualTypeOf>() + expectTypeOf(project.department_id).toEqualTypeOf< + RefLeaf + >() + + return and( + eq(project.user_id, user.id), + eq(project.department_id, dept.id), + ) + }, + ), + ) }) }) @@ -359,8 +375,11 @@ describe(`Query Builder Callback Types`, () => { test(`orderBy with joins`, () => { new Query() .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .orderBy(({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() expectTypeOf(dept.id).toEqualTypeOf>() @@ -398,8 +417,11 @@ describe(`Query Builder Callback Types`, () => { test(`groupBy with joins`, () => { new Query() .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .groupBy(({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() expectTypeOf(dept.id).toEqualTypeOf>() @@ -478,8 +500,11 @@ describe(`Query Builder Callback Types`, () => { test(`having with joins`, () => { new Query() .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id))) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ), + ) .groupBy(({ dept }) => dept.location) .having(({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() @@ -495,20 +520,29 @@ describe(`Query Builder Callback Types`, () => { test(`complex query with all callback types`, () => { new Query() .from({ user: usersCollection }) - .join(leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { - expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept.id).toEqualTypeOf>() - expectTypeOf(dept.location).toEqualTypeOf>() - return eq(user.department_id, dept.id) - })) - .join(leftJoin({ project: projectsCollection }, ({ user, dept, project }) => { - expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept.id).toEqualTypeOf>() - expectTypeOf(dept.location).toEqualTypeOf>() - expectTypeOf(project.user_id).toEqualTypeOf>() - expectTypeOf(project.department_id).toEqualTypeOf>() - return eq(project.user_id, user.id) - })) + .join( + leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { + expectTypeOf(user.id).toEqualTypeOf>() + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(dept.location).toEqualTypeOf>() + return eq(user.department_id, dept.id) + }), + ) + .join( + leftJoin( + { project: projectsCollection }, + ({ user, dept, project }) => { + expectTypeOf(user.id).toEqualTypeOf>() + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(dept.location).toEqualTypeOf>() + expectTypeOf(project.user_id).toEqualTypeOf>() + expectTypeOf(project.department_id).toEqualTypeOf< + RefLeaf + >() + return eq(project.user_id, user.id) + }, + ), + ) .where(({ user, dept, project }) => { expectTypeOf(user.id).toEqualTypeOf>() expectTypeOf(dept.id).toEqualTypeOf>() diff --git a/packages/db/tests/query/builder/functional-variants.test.ts b/packages/db/tests/query/builder/functional-variants.test.ts index 77a67e8772..346c37c621 100644 --- a/packages/db/tests/query/builder/functional-variants.test.ts +++ b/packages/db/tests/query/builder/functional-variants.test.ts @@ -73,9 +73,13 @@ describe(`QueryBuilder functional variants (fn)`, () => { it(`works with joins`, () => { const query = new Query() .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .fn.select((row) => ({ employeeName: row.employees.name, departmentName: row.departments?.name || `No Department`, @@ -141,9 +145,13 @@ describe(`QueryBuilder functional variants (fn)`, () => { it(`works with joins`, () => { const query = new Query() .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .fn.where( (row) => row.employees.active && @@ -213,9 +221,13 @@ describe(`QueryBuilder functional variants (fn)`, () => { it(`works with joins and grouping`, () => { const query = new Query() .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .groupBy(({ departments }) => departments.name) .fn.having( (row) => @@ -233,9 +245,13 @@ describe(`QueryBuilder functional variants (fn)`, () => { it(`supports all functional variants together`, () => { const query = new Query() .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .fn.where((row) => row.employees.active) .fn.where((row) => row.employees.salary > 40000) .groupBy(({ departments }) => departments.name) diff --git a/packages/db/tests/query/builder/functions.test.ts b/packages/db/tests/query/builder/functions.test.ts index e4d284960b..1876596dc5 100644 --- a/packages/db/tests/query/builder/functions.test.ts +++ b/packages/db/tests/query/builder/functions.test.ts @@ -29,9 +29,9 @@ import { or, subtract, sum, - toArray, upper, } from '../../../src/query/builder/functions.js' +import { toArray } from '../../../src/query/builder/include-functions.js' import { compileSingleRowExpression } from '../../../src/query/compiler/evaluators.js' import type { BasicExpression } from '../../../src/query/ir.js' diff --git a/packages/db/tests/query/builder/join.test.ts b/packages/db/tests/query/builder/join.test.ts index 716a8c1784..464dcdd508 100644 --- a/packages/db/tests/query/builder/join.test.ts +++ b/packages/db/tests/query/builder/join.test.ts @@ -38,9 +38,13 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) const builtQuery = getQueryIR(query) expect(builtQuery.join).toBeDefined() @@ -69,11 +73,20 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) - .join(leftJoin({ projects: projectsCollection }, ({ departments, projects }) => - eq(departments.id, projects.department_id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) + .join( + leftJoin( + { projects: projectsCollection }, + ({ departments, projects }) => + eq(departments.id, projects.department_id), + ), + ) const builtQuery = getQueryIR(query) expect(builtQuery.join).toBeDefined() @@ -90,9 +103,13 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .select(({ employees, departments }) => ({ id: employees.id, name: employees.name, @@ -112,9 +129,13 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .where(({ departments }) => gt(departments.budget, 1000000)) const builtQuery = getQueryIR(query) @@ -130,8 +151,11 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join(leftJoin({ bigDepts: subQuery as any }, ({ employees, bigDepts }) => - eq(employees.department_id, (bigDepts as any).id))) + .join( + leftJoin({ bigDepts: subQuery as any }, ({ employees, bigDepts }) => + eq(employees.department_id, (bigDepts as any).id), + ), + ) const builtQuery = getQueryIR(query) expect(builtQuery.join).toBeDefined() @@ -146,9 +170,13 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .where(({ employees, departments }) => and(gt(employees.salary, 50000), gt(departments.budget, 1000000)), ) @@ -183,11 +211,18 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) - .join(leftJoin({ users: usersCollection }, ({ employees, users }) => - eq(employees.id, users.employee_id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) + .join( + leftJoin({ users: usersCollection }, ({ employees, users }) => + eq(employees.id, users.employee_id), + ), + ) const builtQuery = getQueryIR(query) expect(builtQuery.join).toBeDefined() @@ -204,9 +239,13 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .select(({ employees, departments }) => ({ employee: employees, department: departments, @@ -223,15 +262,23 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const explicitQuery = builder .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) const aliasQuery = builder .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) const explicitQueryIR = getQueryIR(explicitQuery) const aliasQueryIR = getQueryIR(aliasQuery) @@ -244,15 +291,23 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const explicitQuery = builder .from({ employees: employeesCollection }) - .join(rightJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + rightJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) const aliasQuery = builder .from({ employees: employeesCollection }) - .join(rightJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + rightJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) const explicitQueryIR = getQueryIR(explicitQuery) const aliasQueryIR = getQueryIR(aliasQuery) @@ -265,15 +320,23 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const explicitQuery = builder .from({ employees: employeesCollection }) - .join(innerJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + innerJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) const aliasQuery = builder .from({ employees: employeesCollection }) - .join(innerJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + innerJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) const explicitQueryIR = getQueryIR(explicitQuery) const aliasQueryIR = getQueryIR(aliasQuery) @@ -286,15 +349,23 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const explicitQuery = builder .from({ employees: employeesCollection }) - .join(fullJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + fullJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) const aliasQuery = builder .from({ employees: employeesCollection }) - .join(fullJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + fullJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) const explicitQueryIR = getQueryIR(explicitQuery) const aliasQueryIR = getQueryIR(aliasQuery) @@ -317,12 +388,20 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join(leftJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) - .join(innerJoin({ projects: projectsCollection }, - ({ departments, projects }) => - eq(departments.id, projects.department_id))) + .join( + leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) + .join( + innerJoin( + { projects: projectsCollection }, + ({ departments, projects }) => + eq(departments.id, projects.department_id), + ), + ) const builtQuery = getQueryIR(query) expect(builtQuery.join).toBeDefined() @@ -341,9 +420,13 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join(innerJoin({ departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + innerJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) .where(({ departments }) => gt(departments.budget, 1000000)) .select(({ employees, departments }) => ({ id: employees.id, diff --git a/packages/db/tests/query/builder/ref-proxy.test.ts b/packages/db/tests/query/builder/ref-proxy.test.ts index 531ca254a1..abc464524c 100644 --- a/packages/db/tests/query/builder/ref-proxy.test.ts +++ b/packages/db/tests/query/builder/ref-proxy.test.ts @@ -281,9 +281,9 @@ describe(`ref-proxy`, () => { expect(expression).toBeInstanceOf(Value) expect((expression as Value).value).toBe(literal) } - expect(() => toExpression(new MaterializeWrapper({} as any))).toThrow( - /materialize\(\) cannot be used inside expressions/, - ) + expect(() => + toExpression(new MaterializeWrapper({} as any, {} as any)), + ).toThrow(/materialize\(\) cannot be used inside expressions/) }) }) diff --git a/packages/db/tests/query/builder/subqueries.test-d.ts b/packages/db/tests/query/builder/subqueries.test-d.ts index 27566101cd..14690f59a1 100644 --- a/packages/db/tests/query/builder/subqueries.test-d.ts +++ b/packages/db/tests/query/builder/subqueries.test-d.ts @@ -119,8 +119,11 @@ describe(`Subquery Types`, () => { // This should work WITHOUT any cast const _query = new Query() .from({ issue: issuesCollection }) - .join(leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id))) + .join( + leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), + ), + ) .select(({ issue, activeUser }) => ({ issueId: issue.id, issueTitle: issue.title, @@ -150,8 +153,11 @@ describe(`Subquery Types`, () => { // This should work WITHOUT any cast const _query = new Query() .from({ issue: issuesCollection }) - .join(leftJoin({ activeUser: userNamesQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id))) + .join( + leftJoin({ activeUser: userNamesQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), + ), + ) .select(({ issue, activeUser }) => ({ issueId: issue.id, userName: activeUser.name, @@ -259,8 +265,11 @@ describe(`Subquery Types`, () => { // Join regular collection with subquery - NO CAST! const _query = new Query() .from({ issue: issuesCollection }) - .join(leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id))) + .join( + leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), + ), + ) .select(({ issue, activeUser }) => ({ issueId: issue.id, userName: activeUser.name, @@ -284,8 +293,11 @@ describe(`Subquery Types`, () => { // Join subquery with regular collection - NO CAST! const _query = new Query() .from({ issue: filteredIssues }) - .join(leftJoin({ user: usersCollection }, ({ issue, user }) => - eq(issue.userId, user.id))) + .join( + leftJoin({ user: usersCollection }, ({ issue, user }) => + eq(issue.userId, user.id), + ), + ) .select(({ issue, user }) => ({ issueId: issue.id, userName: user.name, diff --git a/packages/db/tests/query/builder/union-all.test.ts b/packages/db/tests/query/builder/union-all.test.ts index 9c96cbc93a..ada09739c7 100644 --- a/packages/db/tests/query/builder/union-all.test.ts +++ b/packages/db/tests/query/builder/union-all.test.ts @@ -118,9 +118,13 @@ describe(`QueryBuilder.unionAll`, () => { const query = builder .unionAll({ employees: employeeDescriptor }) - .join(innerJoin({ departments: departmentDescriptor }, - ({ employees, departments }) => - eq(employees.department_id, departments.id))) + .join( + innerJoin( + { departments: departmentDescriptor }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + ), + ) expect(collectSourceRefs(getQueryIR(query)).map((ref) => ref.type)).toEqual( [`descriptorRef`, `descriptorRef`], @@ -157,8 +161,12 @@ describe(`QueryBuilder.unionAll`, () => { const query = builder .unionAll(employeeRows, departmentRows) - .join(innerJoin({ departments: departmentDescriptor }, - ({ id, departments }) => eq(id, departments.id))) + .join( + innerJoin( + { departments: departmentDescriptor }, + ({ id, departments }) => eq(id, departments.id), + ), + ) expect(collectSourceRefs(getQueryIR(query)).map((ref) => ref.type)).toEqual( [`descriptorRef`, `descriptorRef`, `descriptorRef`], diff --git a/packages/db/tests/query/case-when.test-d.ts b/packages/db/tests/query/case-when.test-d.ts index 1dc9a98dc1..5aa3b0d008 100644 --- a/packages/db/tests/query/case-when.test-d.ts +++ b/packages/db/tests/query/case-when.test-d.ts @@ -1,12 +1,11 @@ import { describe, expectTypeOf, test } from 'vitest' -import { - caseWhen, +import { caseWhen, createLiveQueryCollection, eq, gt, + include, leftJoin, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -190,12 +189,14 @@ describe(`caseWhen types`, () => { id: user.id, adultProfile: caseWhen(gt(user.age, 18), { id: user.id, - posts: q - .from({ post: posts }) - .where(({ post }) => eq(post.userId, user.id)) - .select(({ post }) => ({ - title: post.title, - })), + posts: include( + q + .from({ post: posts }) + .where(({ post }) => eq(post.userId, user.id)) + .select(({ post }) => ({ + title: post.title, + })), + ), }), })), ) diff --git a/packages/db/tests/query/case-when.test.ts b/packages/db/tests/query/case-when.test.ts index 8a155ffde3..ba2d0b7c2c 100644 --- a/packages/db/tests/query/case-when.test.ts +++ b/packages/db/tests/query/case-when.test.ts @@ -5,6 +5,7 @@ import { createLiveQueryCollection, eq, gt, + include, innerJoin, leftJoin, toArray, @@ -658,13 +659,15 @@ describe(`caseWhen`, () => { id: user.id, name: user.name, label: `adult`, - posts: q - .from({ post: posts }) - .where(({ post }) => eq(post.userId, user.id)) - .orderBy(({ post }) => post.id) - .select(({ post }) => ({ - title: post.title, - })), + posts: include( + q + .from({ post: posts }) + .where(({ post }) => eq(post.userId, user.id)) + .orderBy(({ post }) => post.id) + .select(({ post }) => ({ + title: post.title, + })), + ), } return { diff --git a/packages/db/tests/query/collection-ref-compatibility.test.ts b/packages/db/tests/query/collection-ref-compatibility.test.ts index 51513bf4b5..9bec013bc5 100644 --- a/packages/db/tests/query/collection-ref-compatibility.test.ts +++ b/packages/db/tests/query/collection-ref-compatibility.test.ts @@ -1,4 +1,6 @@ import { describe, expect, it } from 'vitest' +import { includesRuntime } from '../../src/query/live/includes-runtime.js' +import { materializeCompilation } from '../../src/query/live/materialized-pipeline.js' import { applyOptimizations } from '../../src/query/optimizer-joins.js' import { processJoins } from '../../src/query/compiler/joins.js' import { @@ -109,6 +111,7 @@ describe(`CollectionRef public shape`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: ref(`joined`), type: `inner`, on: new IR.Func(`eq`, [ @@ -123,6 +126,7 @@ describe(`CollectionRef public shape`, () => { new IR.PropRef([`root`, `id`]), new IR.PropRef([`child`, `id`]), `children`, + includesRuntime, ), }, }, diff --git a/packages/db/tests/query/compiler/basic.test.ts b/packages/db/tests/query/compiler/basic.test.ts index 687c398efd..ddee4bafff 100644 --- a/packages/db/tests/query/compiler/basic.test.ts +++ b/packages/db/tests/query/compiler/basic.test.ts @@ -213,7 +213,10 @@ describe(`Query2 Compiler`, () => { const query: QueryIR = { from: new CollectionRef(usersCollection, userAlias), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, + materialize: materializeCompilation, type: `inner`, from: new CollectionRef(postsCollection, postAlias), on: new Func('eq', [ diff --git a/packages/db/tests/query/compiler/group-by-pipeline.test.ts b/packages/db/tests/query/compiler/group-by-pipeline.test.ts index ddd13fb250..03abcd6fc8 100644 --- a/packages/db/tests/query/compiler/group-by-pipeline.test.ts +++ b/packages/db/tests/query/compiler/group-by-pipeline.test.ts @@ -134,7 +134,11 @@ describe(`group-by production pipeline`, () => { const graph = new D2() const input = graph.newInput() const groupRef = new PropRef([`row`, `group`]) - const total = new Aggregate(`sum`, [new PropRef([`row`, `amount`])], grouping) + const total = new Aggregate( + `sum`, + [new PropRef([`row`, `amount`])], + grouping, + ) // Exercise generated-field collision avoidance as well as wrapped refs. const totalAlias = `__tanstack_group_synced` const select: Select | undefined = diff --git a/packages/db/tests/query/compiler/group-by.test.ts b/packages/db/tests/query/compiler/group-by.test.ts index ad6cecf116..0fd9521814 100644 --- a/packages/db/tests/query/compiler/group-by.test.ts +++ b/packages/db/tests/query/compiler/group-by.test.ts @@ -74,8 +74,16 @@ describe(`group-by compiler`, () => { const groupByClause = [new PropRef([`users`, `department`])] const selectClause = { department: new PropRef([`users`, `department`]), - count: new Aggregate(`count`, [new PropRef([`users`, `id`])], grouping), - avg_salary: new Aggregate(`avg`, [new PropRef([`users`, `salary`])], grouping), + count: new Aggregate( + `count`, + [new PropRef([`users`, `id`])], + grouping, + ), + avg_salary: new Aggregate( + `avg`, + [new PropRef([`users`, `salary`])], + grouping, + ), } // Should not throw diff --git a/packages/db/tests/query/compiler/lazy-demand.test.ts b/packages/db/tests/query/compiler/lazy-demand.test.ts index 21e54c26dd..dab66c5473 100644 --- a/packages/db/tests/query/compiler/lazy-demand.test.ts +++ b/packages/db/tests/query/compiler/lazy-demand.test.ts @@ -1,5 +1,6 @@ import { D2, output } from '@tanstack/db-ivm' import { describe, expect, it } from 'vitest' +import { materializeCompilation } from '../../../src/query/live/materialized-pipeline.js' import { applyOptimizations } from '../../../src/query/optimizer-joins.js' import { processJoins } from '../../../src/query/compiler/joins.js' import { createCollection } from '../../../src/collection/index.js' @@ -31,7 +32,10 @@ function createDemandHarness( { from: new CollectionRef(left, `left`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, + materialize: materializeCompilation, type: joinType, from: new CollectionRef(right, `right`), on: diff --git a/packages/db/tests/query/compiler/select.test.ts b/packages/db/tests/query/compiler/select.test.ts index 2a25a34e59..38c4c5b67e 100644 --- a/packages/db/tests/query/compiler/select.test.ts +++ b/packages/db/tests/query/compiler/select.test.ts @@ -34,7 +34,11 @@ describe(`select compiler`, () => { }) it(`throws error for aggregate expressions`, () => { - const arg = new Aggregate(`count`, [new PropRef([`users`, `id`])], grouping) + const arg = new Aggregate( + `count`, + [new PropRef([`users`, `id`])], + grouping, + ) const namespacedRow = { users: { id: 1 } } expect(() => { diff --git a/packages/db/tests/query/compiler/subqueries.test.ts b/packages/db/tests/query/compiler/subqueries.test.ts index 3e220ad8be..cd3e12b17e 100644 --- a/packages/db/tests/query/compiler/subqueries.test.ts +++ b/packages/db/tests/query/compiler/subqueries.test.ts @@ -224,8 +224,11 @@ describe(`Query2 Subqueries`, () => { // Use the subquery in a join const query = new Query() .from({ issue: issuesCollection }) - .join(leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id))) + .join( + leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), + ), + ) .select(({ issue, activeUser }) => ({ issueId: issue.id, issueTitle: issue.title, @@ -258,8 +261,11 @@ describe(`Query2 Subqueries`, () => { // Use the subquery in a join const query = new Query() .from({ issue: issuesCollection }) - .join(leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id))) + .join( + leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), + ), + ) .select(({ issue, activeUser }) => ({ issueId: issue.id, issueTitle: issue.title, @@ -361,8 +367,11 @@ describe(`Query2 Subqueries`, () => { const outerQuery = new Query() .from({ issue: issuesCollection }) - .join(leftJoin({ userInfo: middleQuery }, ({ issue, userInfo }) => - eq(issue.userId, userInfo.id))) + .join( + leftJoin({ userInfo: middleQuery }, ({ issue, userInfo }) => + eq(issue.userId, userInfo.id), + ), + ) .select(({ issue, userInfo }) => ({ issueId: issue.id, issueTitle: issue.title, diff --git a/packages/db/tests/query/compiler/subquery-caching.test.ts b/packages/db/tests/query/compiler/subquery-caching.test.ts index 089642e3f2..fe1d48c2dd 100644 --- a/packages/db/tests/query/compiler/subquery-caching.test.ts +++ b/packages/db/tests/query/compiler/subquery-caching.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { D2 } from '@tanstack/db-ivm' +import { materializeCompilation } from '../../../src/query/live/materialized-pipeline.js' import { applyOptimizations } from '../../../src/query/optimizer-joins.js' import { processJoins } from '../../../src/query/compiler/joins.js' import { compileQuery } from '../../../src/query/compiler/index.js' @@ -105,7 +106,10 @@ describe(`Subquery Caching`, () => { const mainQuery: QueryIR = { from: new QueryRef(subquery, `main_users`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, + materialize: materializeCompilation, type: `inner`, from: new QueryRef(subquery, `joined_users`), // Same subquery object reference on: new Func('eq', [ @@ -381,7 +385,10 @@ describe(`Subquery Caching`, () => { const middleSubquery: QueryIR = { from: new QueryRef(innerSubquery, `inner1`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, + materialize: materializeCompilation, type: `left`, from: new QueryRef(innerSubquery, `inner2`), // Same innerSubquery on: new Func('eq', [ @@ -395,7 +402,10 @@ describe(`Subquery Caching`, () => { const outerQuery: QueryIR = { from: new QueryRef(middleSubquery, `middle`), join: [ - { compile: processJoins, optimize: applyOptimizations, + { + compile: processJoins, + optimize: applyOptimizations, + materialize: materializeCompilation, type: `inner`, from: new QueryRef(innerSubquery, `direct`), // innerSubquery again at top level on: new Func('eq', [ diff --git a/packages/db/tests/query/functional-variants.test-d.ts b/packages/db/tests/query/functional-variants.test-d.ts index 777b2aa77d..796c494d4e 100644 --- a/packages/db/tests/query/functional-variants.test-d.ts +++ b/packages/db/tests/query/functional-variants.test-d.ts @@ -1,15 +1,14 @@ import { describe, expectTypeOf, test } from 'vitest' -import { - Query, +import { Query, caseWhen, count, createLiveQueryCollection, eq, gt, + include, leftJoin, materialize, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -172,7 +171,7 @@ describe(`Functional Variants Types`, () => { createLiveQueryCollection((q) => q.from({ user: usersCollection }).fn.select((row) => ({ id: row.user.id, - departments: q.from({ department: departmentsCollection }), + departments: include(q.from({ department: departmentsCollection })), })), ) diff --git a/packages/db/tests/query/group-by-work.test.ts b/packages/db/tests/query/group-by-work.test.ts index dbdc0a6d87..148025bd0f 100644 --- a/packages/db/tests/query/group-by-work.test.ts +++ b/packages/db/tests/query/group-by-work.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' +import { createLiveQueryCollection, eq, include } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' -import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { count, max, sum } from '../../src/query/builder/functions.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -194,10 +194,12 @@ function createCommentCount(size: number) { q.from({ issue: issues }).select(({ issue }) => ({ id: issue.id, title: issue.title, - comments: q - .from({ comment: comments }) - .where(({ comment }) => eq(comment.issueId, issue.id)) - .select(({ comment }) => ({ n: count(comment.id) })), + comments: include( + q + .from({ comment: comments }) + .where(({ comment }) => eq(comment.issueId, issue.id)) + .select(({ comment }) => ({ n: count(comment.id) })), + ), })), ) const write = ( diff --git a/packages/db/tests/query/identity-output-shape-oracle.test.ts b/packages/db/tests/query/identity-output-shape-oracle.test.ts index d83d2e863f..a497d4208f 100644 --- a/packages/db/tests/query/identity-output-shape-oracle.test.ts +++ b/packages/db/tests/query/identity-output-shape-oracle.test.ts @@ -3,6 +3,7 @@ import { D2, MultiSet, output, serializeValue } from '@tanstack/db-ivm' import { Temporal } from 'temporal-polyfill' import fc from 'fast-check' import { describe, expect, it } from 'vitest' +import { materializeCompilation } from '../../src/query/live/materialized-pipeline.js' import { Query, and, @@ -160,6 +161,7 @@ async function runShapes( { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: 'inner', from: post, on: new Func('eq', [ diff --git a/packages/db/tests/query/includes-collection-oracle.property.test.ts b/packages/db/tests/query/includes-collection-oracle.property.test.ts index 885dfb913e..b3aafd89e1 100644 --- a/packages/db/tests/query/includes-collection-oracle.property.test.ts +++ b/packages/db/tests/query/includes-collection-oracle.property.test.ts @@ -1,21 +1,20 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect } from 'vitest' -import { - add, +import { add, caseWhen, concat, count, createLiveQueryCollection, eq, gt, + include, innerJoin, leftJoin, lt, materialize, multiply, sum, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { runTrace } from '../trace-runner.js' @@ -221,7 +220,7 @@ function createCollectionQuery( return { id: parent.id, group: parent.group, - children: childRows, + children: include(childRows), arrayChildren: toArray( reuseChildRelation ? childRows : createChildRows(), ), @@ -1242,10 +1241,12 @@ describe(`Collection-valued includes oracle`, () => { .select(({ parent }) => ({ id: parent.id, value: parent.value, - children: q - .from({ child: nodes.collection }) - .where(({ child }) => eq(child.kind, `child`)) - .where(({ child }) => eq(child.group, parent.group)), + children: include( + q + .from({ child: nodes.collection }) + .where(({ child }) => eq(child.kind, `child`)) + .where(({ child }) => eq(child.group, parent.group)), + ), })), ) @@ -1334,10 +1335,12 @@ describe(`Collection-valued includes oracle`, () => { .select(({ parent }) => ({ id: parent.id, value: parent.value, - children: q - .from({ child: nodes.collection }) - .where(({ child }) => eq(child.kind, `child`)) - .where(({ child }) => eq(child.group, parent.group)), + children: include( + q + .from({ child: nodes.collection }) + .where(({ child }) => eq(child.kind, `child`)) + .where(({ child }) => eq(child.group, parent.group)), + ), })), ) @@ -1388,9 +1391,11 @@ describe(`Collection-valued includes oracle`, () => { const live = createLiveQueryCollection((q) => q.from({ parent: parents.collection }).select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children.collection }) - .where(({ child }) => eq(child.parentGroup, parent.group)), + children: include( + q + .from({ child: children.collection }) + .where(({ child }) => eq(child.parentGroup, parent.group)), + ), })), ) @@ -1536,11 +1541,13 @@ describe(`Collection-valued includes oracle`, () => { .select(({ message }) => ({ kind: `message` as const, id: message.id, - children: q - .from({ messageChild: children.collection }) - .where(({ messageChild }) => - eq(messageChild.parentGroup, message.group), - ), + children: include( + q + .from({ messageChild: children.collection }) + .where(({ messageChild }) => + eq(messageChild.parentGroup, message.group), + ), + ), })) const toolRows = q .from({ tool: tools.collection }) @@ -1793,10 +1800,12 @@ describe(`Collection-valued includes oracle`, () => { .select(({ parent }) => ({ id: parent.id, value: parent.value, - children: q - .from({ child: nodes.collection }) - .where(({ child }) => eq(child.kind, `child`)) - .where(({ child }) => eq(child.group, parent.group)), + children: include( + q + .from({ child: nodes.collection }) + .where(({ child }) => eq(child.kind, `child`)) + .where(({ child }) => eq(child.group, parent.group)), + ), })), ) @@ -1870,10 +1879,12 @@ describe(`Collection-valued includes oracle`, () => { .select(({ parent }) => ({ id: parent.id, value: parent.value, - children: q - .from({ child: nodes.collection }) - .where(({ child }) => eq(child.kind, `child`)) - .where(({ child }) => eq(child.group, parent.group)), + children: include( + q + .from({ child: nodes.collection }) + .where(({ child }) => eq(child.kind, `child`)) + .where(({ child }) => eq(child.group, parent.group)), + ), })), ) @@ -2039,7 +2050,7 @@ describe(`Collection-valued includes oracle`, () => { return { id: parent.id, - facade: childRows(), + facade: include(childRows()), array: toArray(childRows()), materialized: materialize(childRows()), first: materialize(childRows().findOne()), @@ -2142,7 +2153,7 @@ describe(`Collection-valued includes oracle`, () => { return { id: parent.id, - facade: childRows(), + facade: include(childRows()), array: toArray(childRows()), materialized: materialize(childRows()), first: materialize(childRows().findOne()), @@ -2323,11 +2334,13 @@ describe(`Collection-valued includes oracle`, () => { const live = createLiveQueryCollection((q) => q.from({ parent: parents.collection }).select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: orderedChildren }) - .where(({ child }) => eq(child.parentGroup, parent.group)) - .orderBy(({ child }) => child.rank) - .select(({ child }) => ({ id: child.id, value: child.value })), + children: include( + q + .from({ child: orderedChildren }) + .where(({ child }) => eq(child.parentGroup, parent.group)) + .orderBy(({ child }) => child.rank) + .select(({ child }) => ({ id: child.id, value: child.value })), + ), })), ) const callbacks: Array<{ @@ -2515,12 +2528,14 @@ describe(`Collection-valued includes oracle`, () => { const live = createLiveQueryCollection((q) => q.from({ parent: parents.collection }).select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.parentGroup, parent.group)) - .orderBy(({ child }) => child.rank) - .limit(2) - .select(({ child }) => ({ id: child.id, value: child.value })), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.parentGroup, parent.group)) + .orderBy(({ child }) => child.rank) + .limit(2) + .select(({ child }) => ({ id: child.id, value: child.value })), + ), })), ) const callbacks: Array<{ @@ -2680,7 +2695,7 @@ describe(`Collection-valued includes oracle`, () => { eq(parent.active, true), { kind: `active` as const, - facade: childRows(), + facade: include(childRows()), array: toArray(childRows()), materialized: materialize(childRows()), }, @@ -2786,7 +2801,7 @@ describe(`Collection-valued includes oracle`, () => { return { id: parent.id, - facade: summaries(), + facade: include(summaries()), array: toArray(summaries()), materialized: materialize(summaries()), implicit: materialize(total()), @@ -2892,7 +2907,7 @@ describe(`Collection-valued includes oracle`, () => { return { id: parent.id, - facade: summaries(), + facade: include(summaries()), array: toArray(summaries()), materialized: materialize(summaries()), } @@ -2965,7 +2980,7 @@ describe(`Collection-valued includes oracle`, () => { return { id: parent.id, - facade: childRows(), + facade: include(childRows()), array: toArray(childRows()), materialized: materialize(childRows()), } @@ -3032,7 +3047,7 @@ describe(`Collection-valued includes oracle`, () => { return { id: parent.id, - facade: childRows(), + facade: include(childRows()), array: toArray(childRows()), materialized: materialize(childRows()), } @@ -3126,7 +3141,7 @@ describe(`Collection-valued includes oracle`, () => { return { id: parent.id, - facade: childRows(), + facade: include(childRows()), array: toArray(childRows()), materialized: materialize(childRows()), } diff --git a/packages/db/tests/query/includes-context-transport-oracle.test.ts b/packages/db/tests/query/includes-context-transport-oracle.test.ts index d5b75d5bfc..161519315c 100644 --- a/packages/db/tests/query/includes-context-transport-oracle.test.ts +++ b/packages/db/tests/query/includes-context-transport-oracle.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'vitest' -import { add, +import { + add, and, caseWhen, coalesce, @@ -8,13 +9,15 @@ import { add, eq, groupKeys, gt, + include, innerJoin, lt, lte, materialize, multiply, sum, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { attachRouteMetadata, stripInternalRouteMetadata, @@ -560,7 +563,7 @@ function includeInEveryForm( query: QueryBuilder, ) { return { - collection: query, + collection: include(query), array: toArray(query), materialized: materialize(query), } diff --git a/packages/db/tests/query/includes-functional-input-boundary.test.ts b/packages/db/tests/query/includes-functional-input-boundary.test.ts index 0e47a21a86..9036a2fe57 100644 --- a/packages/db/tests/query/includes-functional-input-boundary.test.ts +++ b/packages/db/tests/query/includes-functional-input-boundary.test.ts @@ -1,10 +1,9 @@ import { describe, expect, it } from 'vitest' -import { - createLiveQueryCollection, +import { createLiveQueryCollection, eq, + include, materialize, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { createControlledCollection } from './includes-oracle-helpers.js' describe(`functional include input boundary`, () => { @@ -180,9 +179,11 @@ describe(`functional include input boundary`, () => { .from({ parent: parents.collection }) .select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children.collection }) - .where(({ child }) => eq(child.parentId, parent.id)), + children: include( + q + .from({ child: children.collection }) + .where(({ child }) => eq(child.parentId, parent.id)), + ), })), }) .fn.select(({ row }) => { @@ -287,9 +288,11 @@ describe(`functional include input boundary`, () => { .from({ row: q.from({ parent: parents.collection }).select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children.collection }) - .where(({ child }) => eq(child.parentId, parent.id)), + children: include( + q + .from({ child: children.collection }) + .where(({ child }) => eq(child.parentId, parent.id)), + ), })), }) .select(({ row }) => ({ id: row.id, children: row.children })), @@ -323,9 +326,11 @@ describe(`functional include input boundary`, () => { return q.from({ row: projected }).select(({ row }) => ({ id: row.id, label: row.label, - children: q - .from({ child: children.collection }) - .where(({ child }) => eq(child.parentId, row.id)), + children: include( + q + .from({ child: children.collection }) + .where(({ child }) => eq(child.parentId, row.id)), + ), })) }) let publications = 0 @@ -377,7 +382,9 @@ describe(`functional include input boundary`, () => { .where(({ leaf }) => eq(leaf.childId, child.id)) return { id: child.id, - leaves: inline ? toArray(leafQuery) : leafQuery, + leaves: inline + ? toArray(leafQuery) + : include(leafQuery), } }), ), diff --git a/packages/db/tests/query/includes-functional-projection-oracle.test.ts b/packages/db/tests/query/includes-functional-projection-oracle.test.ts index 1ecabac7dd..7ba45e2be6 100644 --- a/packages/db/tests/query/includes-functional-projection-oracle.test.ts +++ b/packages/db/tests/query/includes-functional-projection-oracle.test.ts @@ -1,14 +1,13 @@ import { describe, expect, it, vi } from 'vitest' +import { createLiveQueryCollection, + eq, + include, + materialize, + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { BasicIndex } from '../../src/indexes/basic-index.js' import { BucketFacadeAdapter } from '../../src/query/live/bucket-facade-adapter.js' -import { - createLiveQueryCollection, - eq, - materialize, - toArray, -} from '../../src/query/index.js' import { flushPromises } from '../utils.js' import { createControlledCollection } from './includes-oracle-helpers.js' @@ -262,9 +261,11 @@ describe(`functional projection output compatibility`, () => { .from({ parent: parents.collection }) .select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)), + ), })), }) return projection === `expression` @@ -449,9 +450,11 @@ describe(`functional projection output compatibility`, () => { return q.from({ row: projected }).select(({ row }) => ({ id: row.id, groupId: row.groupId, - children: q - .from({ child: children.collection }) - .where(({ child }) => eq(child.groupId, row.groupId)), + children: include( + q + .from({ child: children.collection }) + .where(({ child }) => eq(child.groupId, row.groupId)), + ), })) }) if (subscribeAt === `publication`) { @@ -592,9 +595,11 @@ describe(`functional projection output compatibility`, () => { return { id: parent.id, groupId: parent.groupId, - children: ordered - ? childQuery.orderBy(({ child }) => child.id, `desc`) - : childQuery, + children: include( + ordered + ? childQuery.orderBy(({ child }) => child.id, `desc`) + : childQuery, + ), } }), }) @@ -762,9 +767,11 @@ describe(`functional projection output compatibility`, () => { row: q.from({ parent: parents.collection }).select(({ parent }) => ({ id: parent.id, label: parent.label, - children: q - .from({ child: childSource.collection }) - .where(({ child }) => eq(child.groupId, parent.groupId)), + children: include( + q + .from({ child: childSource.collection }) + .where(({ child }) => eq(child.groupId, parent.groupId)), + ), })), }) return source.select(({ row }) => ({ @@ -861,9 +868,11 @@ describe(`functional projection output compatibility`, () => { }) return q.from({ row: projected }).select(({ row }) => ({ id: row.id, - children: q - .from({ child: children.collection }) - .where(({ child }) => eq(child.groupId, row.groupId)), + children: include( + q + .from({ child: children.collection }) + .where(({ child }) => eq(child.groupId, row.groupId)), + ), })) }) let primaryFailure: unknown = noPrimaryFailure @@ -921,9 +930,11 @@ describe(`functional projection output compatibility`, () => { const source = createLiveQueryCollection((q) => q.from({ parent: parents.collection }).select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children.collection }) - .where(({ child }) => eq(child.parentId, parent.id)), + children: include( + q + .from({ child: children.collection }) + .where(({ child }) => eq(child.parentId, parent.id)), + ), })), ) const projected = createLiveQueryCollection((q) => @@ -1016,7 +1027,7 @@ describe(`functional projection output compatibility`, () => { base: parent.base, children: form === `collection` - ? childRows + ? include(childRows) : form === `array` ? toArray(childRows) : materialize(childRows), @@ -1176,7 +1187,7 @@ describe(`functional projection output compatibility`, () => { ? { children: form === `collection` - ? childRows + ? include(childRows) : form === `array` ? toArray(childRows) : materialize(childRows), @@ -1305,7 +1316,7 @@ describe(`functional projection output compatibility`, () => { id: parent.id, children: form === `collection` - ? primary + ? include(primary) : form === `array` ? toArray(primary) : materialize(primary), @@ -1313,7 +1324,7 @@ describe(`functional projection output compatibility`, () => { ? { sibling: form === `collection` - ? sibling + ? include(sibling) : form === `array` ? toArray(sibling) : materialize(sibling), @@ -1632,7 +1643,7 @@ describe(`functional include projection boundary grammar`, () => { total: 0, children: form === `collection` - ? childRows + ? include(childRows) : form === `array` ? toArray(childRows) : materialize(childRows), diff --git a/packages/db/tests/query/includes-lazy-loading.test.ts b/packages/db/tests/query/includes-lazy-loading.test.ts index 3c2476acc0..c0b7ce2f94 100644 --- a/packages/db/tests/query/includes-lazy-loading.test.ts +++ b/packages/db/tests/query/includes-lazy-loading.test.ts @@ -1,12 +1,11 @@ import { describe, expect, it, vi } from 'vitest' -import { - and, +import { and, createLiveQueryCollection, eq, gte, + include, leftJoin, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { extractSimpleComparisons } from '../../src/query/expression-helpers.js' import { @@ -485,13 +484,15 @@ describe(`includes lazy loading`, () => { q.from({ r: roots }).select(({ r }) => ({ id: r.id, name: r.name, - children: q - .from({ item: items }) - .where(({ item }) => eq(item.rootId, r.id)) - .select(({ item }) => ({ - id: item.id, - title: item.title, - })), + children: include( + q + .from({ item: items }) + .where(({ item }) => eq(item.rootId, r.id)) + .select(({ item }) => ({ + id: item.id, + title: item.title, + })), + ), })), ) diff --git a/packages/db/tests/query/includes-space-oracle-fixture.ts b/packages/db/tests/query/includes-space-oracle-fixture.ts index 1cdebeb8d5..a3910efa91 100644 --- a/packages/db/tests/query/includes-space-oracle-fixture.ts +++ b/packages/db/tests/query/includes-space-oracle-fixture.ts @@ -1,7 +1,7 @@ +import { createLiveQueryCollection, eq, include } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { localOnlyCollectionOptions } from '../../src/local-only.js' -import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { withHistoryCleanup } from '../optimistic-history-oracle.js' type RootRow = { id: string } @@ -98,22 +98,28 @@ export async function createNestedCollectionFixture( const live = createLiveQueryCollection((q) => q.from({ root: sources.roots }).select(({ root }) => ({ id: root.id, - branches: q - .from({ branch: sources.branches }) - .where(({ branch }) => eq(branch.rootId, root.id)) - .select(({ branch }) => ({ - id: branch.id, - twigs: q - .from({ twig: sources.twigs }) - .where(({ twig }) => eq(twig.branchId, branch.id)) - .select(({ twig }) => ({ - id: twig.id, - leaves: q - .from({ leaf: sources.leaves }) - .where(({ leaf }) => eq(leaf.twigId, twig.id)) - .select(({ leaf }) => ({ id: leaf.id })), - })), - })), + branches: include( + q + .from({ branch: sources.branches }) + .where(({ branch }) => eq(branch.rootId, root.id)) + .select(({ branch }) => ({ + id: branch.id, + twigs: include( + q + .from({ twig: sources.twigs }) + .where(({ twig }) => eq(twig.branchId, branch.id)) + .select(({ twig }) => ({ + id: twig.id, + leaves: include( + q + .from({ leaf: sources.leaves }) + .where(({ leaf }) => eq(leaf.twigId, twig.id)) + .select(({ leaf }) => ({ id: leaf.id })), + ), + })), + ), + })), + ), })), ) diff --git a/packages/db/tests/query/includes-work-counter-oracle.test.ts b/packages/db/tests/query/includes-work-counter-oracle.test.ts index a8a60fb958..a65c0f6b75 100644 --- a/packages/db/tests/query/includes-work-counter-oracle.test.ts +++ b/packages/db/tests/query/includes-work-counter-oracle.test.ts @@ -4,6 +4,7 @@ import { createLiveQueryCollection, eq, inArray, + include, innerJoin, leftJoin, materialize, @@ -1121,7 +1122,9 @@ async function checkWindowedIncludeWork(testCase: WindowCase): Promise { groupChildren.select(({ child }) => ({ id: child.id })), ) case `collection`: - return groupChildren.select(({ child }) => ({ id: child.id })) + return include( + groupChildren.select(({ child }) => ({ id: child.id })), + ) case `nested`: return materialize( groupChildren.select(({ child }) => ({ diff --git a/packages/db/tests/query/includes.test-d.ts b/packages/db/tests/query/includes.test-d.ts index da1f6f591f..0a79ad5460 100644 --- a/packages/db/tests/query/includes.test-d.ts +++ b/packages/db/tests/query/includes.test-d.ts @@ -1,13 +1,12 @@ import { describe, expectTypeOf, test } from 'vitest' -import { - Query, +import { Query, concat, createLiveQueryCollection, eq, + include, materialize, queryOnce, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { WithVirtualProps } from '../../src/virtual-props.js' @@ -104,13 +103,15 @@ describe(`includes subquery types`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), })), ) @@ -130,7 +131,9 @@ describe(`includes subquery types`, () => { const collection = createLiveQueryCollection((q) => q.from({ p: projects }).select(({ p }) => ({ id: p.id, - issues: q.from({ i: issues }).where(({ i }) => eq(i.projectId, p.id)), + issues: include( + q.from({ i: issues }).where(({ i }) => eq(i.projectId, p.id)), + ), })), ) @@ -149,20 +152,24 @@ describe(`includes subquery types`, () => { const collection = createLiveQueryCollection((q) => q.from({ p: projects }).select(({ p }) => ({ id: p.id, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - })), - comments: q - .from({ c: comments }) - .where(({ c }) => eq(c.issueId, p.id)) - .select(({ c }) => ({ - id: c.id, - body: c.body, - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), + comments: include( + q + .from({ c: comments }) + .where(({ c }) => eq(c.issueId, p.id)) + .select(({ c }) => ({ + id: c.id, + body: c.body, + })), + ), })), ) @@ -179,20 +186,24 @@ describe(`includes subquery types`, () => { const collection = createLiveQueryCollection((q) => q.from({ p: projects }).select(({ p }) => ({ id: p.id, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - comments: q - .from({ c: comments }) - .where(({ c }) => eq(c.issueId, i.id)) - .select(({ c }) => ({ - id: c.id, - body: c.body, - })), - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + comments: include( + q + .from({ c: comments }) + .where(({ c }) => eq(c.issueId, i.id)) + .select(({ c }) => ({ + id: c.id, + body: c.body, + })), + ), + })), + ), })), ) diff --git a/packages/db/tests/query/includes.test.ts b/packages/db/tests/query/includes.test.ts index 323abe4315..04c62247ff 100644 --- a/packages/db/tests/query/includes.test.ts +++ b/packages/db/tests/query/includes.test.ts @@ -1,12 +1,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { and, + caseWhen, coalesce, concat, count, createLiveQueryCollection, eq, + gt, gte, + include, innerJoin, materialize, toArray, @@ -20,6 +23,7 @@ import { resetCleanupQueue, stripVirtualProps, } from '../utils.js' +import { IncludeOperatorRequiredError } from '../../src/errors.js' import type { SyncConfig } from '../../src/types.js' type Project = { @@ -160,13 +164,15 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), })), ) } @@ -373,10 +379,12 @@ describe(`includes subqueries`, () => { const collection = createLiveQueryCollection((q) => q.from({ parent: parents }).select(({ parent }) => ({ id: parent.id, - items: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)) - .select(({ child }) => ({ id: child.id })), + items: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)) + .select(({ child }) => ({ id: child.id })), + ), })), ) @@ -531,12 +539,14 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - ...i, - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + ...i, + })), + ), })), ) @@ -665,14 +675,16 @@ describe(`includes subqueries`, () => { const collection = createLiveQueryCollection((q) => q.from({ lineItem: lineItems }).select(({ lineItem }) => ({ id: lineItem.id, - product: q - .from({ product: products }) - .where(({ product }) => eq(product.id, lineItem.productId)) - .select(({ product }) => ({ - id: product.id, - categoryId: product.categoryId, - name: product.name, - })), + product: include( + q + .from({ product: products }) + .where(({ product }) => eq(product.id, lineItem.productId)) + .select(({ product }) => ({ + id: product.id, + categoryId: product.categoryId, + name: product.name, + })), + ), })), ) await collection.preload() @@ -792,13 +804,15 @@ describe(`includes subqueries`, () => { .select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), })), ) @@ -823,14 +837,16 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .orderBy(({ i }) => i.title, `desc`) - .select(({ i }) => ({ - id: i.id, - title: i.title, - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .orderBy(({ i }) => i.title, `desc`) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), })), ) @@ -860,14 +876,16 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .orderBy(({ i }) => i.title, `asc`) - .select(({ i }) => ({ - id: i.id, - title: i.title, - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .orderBy(({ i }) => i.title, `asc`) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), })), ) @@ -899,11 +917,13 @@ describe(`includes subqueries`, () => { const collection = createLiveQueryCollection((q) => q.from({ p: projects }).select(({ p }) => ({ id: p.id, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .orderBy(({ i }) => i.title, `asc`) - .select(({ i }) => ({ id: i.id })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .orderBy(({ i }) => i.title, `asc`) + .select(({ i }) => ({ id: i.id })), + ), })), ) @@ -933,15 +953,17 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .orderBy(({ i }) => i.title, `asc`) - .limit(1) - .select(({ i }) => ({ - id: i.id, - title: i.title, - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .orderBy(({ i }) => i.title, `asc`) + .limit(1) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), })), ) @@ -967,15 +989,17 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .orderBy(({ i }) => i.title, `asc`) - .limit(1) - .select(({ i }) => ({ - id: i.id, - title: i.title, - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .orderBy(({ i }) => i.title, `asc`) + .limit(1) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), })), ) @@ -1053,13 +1077,15 @@ describe(`includes subqueries`, () => { id: t.id, name: t.name, departmentId: t.departmentId, - members: q - .from({ m: members }) - .where(({ m }) => eq(m.departmentId, t.departmentId)) - .select(({ m }) => ({ - id: m.id, - name: m.name, - })), + members: include( + q + .from({ m: members }) + .where(({ m }) => eq(m.departmentId, t.departmentId)) + .select(({ m }) => ({ + id: m.id, + name: m.name, + })), + ), })), ) @@ -1103,13 +1129,15 @@ describe(`includes subqueries`, () => { id: t.id, name: t.name, departmentId: t.departmentId, - members: q - .from({ m: members }) - .where(({ m }) => eq(m.departmentId, t.departmentId)) - .select(({ m }) => ({ - id: m.id, - name: m.name, - })), + members: include( + q + .from({ m: members }) + .where(({ m }) => eq(m.departmentId, t.departmentId)) + .select(({ m }) => ({ + id: m.id, + name: m.name, + })), + ), })), ) @@ -1150,13 +1178,15 @@ describe(`includes subqueries`, () => { id: t.id, name: t.name, departmentId: t.departmentId, - members: q - .from({ m: members }) - .where(({ m }) => eq(m.departmentId, t.departmentId)) - .select(({ m }) => ({ - id: m.id, - name: m.name, - })), + members: include( + q + .from({ m: members }) + .where(({ m }) => eq(m.departmentId, t.departmentId)) + .select(({ m }) => ({ + id: m.id, + name: m.name, + })), + ), })), ) @@ -1216,10 +1246,12 @@ describe(`includes subqueries`, () => { q.from({ parent: parents }).select(({ parent }) => ({ id: parent.id, groupId: parent.groupId, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)) - .select(({ child }) => ({ id: child.id })), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)) + .select(({ child }) => ({ id: child.id })), + ), })), ) @@ -1312,10 +1344,12 @@ describe(`includes subqueries`, () => { const collection = createLiveQueryCollection((q) => q.from({ parent: parents }).select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)) - .select(({ child }) => ({ id: child.id })), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)) + .select(({ child }) => ({ id: child.id })), + ), })), ) @@ -1361,10 +1395,12 @@ describe(`includes subqueries`, () => { .limit(1) .select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)) - .select(({ child }) => ({ id: child.id })), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)) + .select(({ child }) => ({ id: child.id })), + ), })), ) @@ -1416,13 +1452,15 @@ describe(`includes subqueries`, () => { .limit(1) .select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)) - .select(({ child }) => ({ - id: child.id, - label: child.label, - })), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)) + .select(({ child }) => ({ + id: child.id, + label: child.label, + })), + ), })), ) const collection = buildQuery() @@ -1498,9 +1536,11 @@ describe(`includes subqueries`, () => { .where(({ parent }) => eq(parent.active, true)) .select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)), + ), })), ) @@ -1548,10 +1588,12 @@ describe(`includes subqueries`, () => { q.from({ parent: parents }).select(({ parent }) => ({ id: parent.id, groupId: parent.groupId, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)) - .select(({ child }) => ({ id: child.id })), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)) + .select(({ child }) => ({ id: child.id })), + ), })), getKey: (row) => { keyReads += 1 @@ -1643,10 +1685,12 @@ describe(`includes subqueries`, () => { .limit(3) .select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)) - .select(({ child }) => ({ id: child.id })), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)) + .select(({ child }) => ({ id: child.id })), + ), })), ) const observations: Array< @@ -1713,10 +1757,12 @@ describe(`includes subqueries`, () => { q.from({ parent: parents }).select(({ parent }) => ({ id: parent.id, groupId: parent.groupId, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)) - .select(({ child }) => ({ id: child.id })), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)) + .select(({ child }) => ({ id: child.id })), + ), })), ) @@ -1786,13 +1832,15 @@ describe(`includes subqueries`, () => { q.from({ t: teams }).select(({ t }) => ({ id: t.id, name: t.name, - members: q - .from({ m: members }) - .where(({ m }) => eq(m.departmentId, t.departmentId)) - .select(({ m }) => ({ - id: m.id, - name: m.name, - })), + members: include( + q + .from({ m: members }) + .where(({ m }) => eq(m.departmentId, t.departmentId)) + .select(({ m }) => ({ + id: m.id, + name: m.name, + })), + ), })), ) @@ -1837,20 +1885,24 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - comments: q - .from({ c: comments }) - .where(({ c }) => eq(c.issueId, i.id)) - .select(({ c }) => ({ - id: c.id, - body: c.body, - })), - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + comments: include( + q + .from({ c: comments }) + .where(({ c }) => eq(c.issueId, i.id)) + .select(({ c }) => ({ + id: c.id, + body: c.body, + })), + ), + })), + ), })), ) } @@ -2229,22 +2281,24 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - comments: toArray( - q - .from({ c: comments }) - .where(({ c }) => eq(c.issueId, i.id)) - .select(({ c }) => ({ - id: c.id, - body: c.body, - })), - ), - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + comments: toArray( + q + .from({ c: comments }) + .where(({ c }) => eq(c.issueId, i.id)) + .select(({ c }) => ({ + id: c.id, + body: c.body, + })), + ), + })), + ), })), ) } @@ -2462,13 +2516,15 @@ describe(`includes subqueries`, () => { .select(({ i }) => ({ id: i.id, title: i.title, - comments: q - .from({ c: comments }) - .where(({ c }) => eq(c.issueId, i.id)) - .select(({ c }) => ({ - id: c.id, - body: c.body, - })), + comments: include( + q + .from({ c: comments }) + .where(({ c }) => eq(c.issueId, i.id)) + .select(({ c }) => ({ + id: c.id, + body: c.body, + })), + ), })), ), })), @@ -2942,15 +2998,17 @@ describe(`includes subqueries`, () => { id: p.id, name: p.name, createdBy: p.createdBy, - issues: q - .from({ i: issuesWC }) - .where(({ i }) => eq(i.projectId, p.id)) - .where(({ i }) => eq(i.createdBy, p.createdBy)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - createdBy: i.createdBy, - })), + issues: include( + q + .from({ i: issuesWC }) + .where(({ i }) => eq(i.projectId, p.id)) + .where(({ i }) => eq(i.createdBy, p.createdBy)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + createdBy: i.createdBy, + })), + ), })), ) @@ -2997,15 +3055,17 @@ describe(`includes subqueries`, () => { id: p.id, name: p.name, createdBy: p.createdBy, - issues: q - .from({ i: issuesWC }) - .where(({ i }) => eq(i.projectId, p.id)) - .where(({ i }) => eq(i.createdBy, p.createdBy)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - createdBy: i.createdBy, - })), + issues: include( + q + .from({ i: issuesWC }) + .where(({ i }) => eq(i.projectId, p.id)) + .where(({ i }) => eq(i.createdBy, p.createdBy)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + createdBy: i.createdBy, + })), + ), })), ) @@ -3037,15 +3097,17 @@ describe(`includes subqueries`, () => { id: p.id, name: p.name, createdBy: p.createdBy, - issues: q - .from({ i: issuesWC }) - .where(({ i }) => eq(i.projectId, p.id)) - .where(({ i }) => eq(i.createdBy, p.createdBy)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - createdBy: i.createdBy, - })), + issues: include( + q + .from({ i: issuesWC }) + .where(({ i }) => eq(i.projectId, p.id)) + .where(({ i }) => eq(i.createdBy, p.createdBy)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + createdBy: i.createdBy, + })), + ), })), ) @@ -3082,16 +3144,18 @@ describe(`includes subqueries`, () => { id: p.id, name: p.name, createdBy: p.createdBy, - issues: q - .from({ i: issuesWC }) - .where(({ i }) => eq(i.projectId, p.id)) - .where(({ i }) => eq(i.createdBy, p.createdBy)) - .where(({ i }) => eq(i.title, `Bug in Alpha`)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - createdBy: i.createdBy, - })), + issues: include( + q + .from({ i: issuesWC }) + .where(({ i }) => eq(i.projectId, p.id)) + .where(({ i }) => eq(i.createdBy, p.createdBy)) + .where(({ i }) => eq(i.title, `Bug in Alpha`)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + createdBy: i.createdBy, + })), + ), })), ) @@ -3115,15 +3179,17 @@ describe(`includes subqueries`, () => { q.from({ p: projectsWC }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issuesWC }) - .where(({ i }) => - and(eq(i.projectId, p.id), eq(i.createdBy, `alice`)), - ) - .select(({ i }) => ({ - id: i.id, - title: i.title, - })), + issues: include( + q + .from({ i: issuesWC }) + .where(({ i }) => + and(eq(i.projectId, p.id), eq(i.createdBy, `alice`)), + ) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), })), ) @@ -3154,16 +3220,18 @@ describe(`includes subqueries`, () => { id: p.id, name: p.name, createdBy: p.createdBy, - issues: q - .from({ i: issuesWC }) - .where(({ i }) => - and(eq(i.projectId, p.id), eq(i.createdBy, p.createdBy)), - ) - .select(({ i }) => ({ - id: i.id, - title: i.title, - createdBy: i.createdBy, - })), + issues: include( + q + .from({ i: issuesWC }) + .where(({ i }) => + and(eq(i.projectId, p.id), eq(i.createdBy, p.createdBy)), + ) + .select(({ i }) => ({ + id: i.id, + title: i.title, + createdBy: i.createdBy, + })), + ), })), ) @@ -3193,16 +3261,18 @@ describe(`includes subqueries`, () => { id: p.id, name: p.name, createdBy: p.createdBy, - issues: q - .from({ i: issuesWC }) - .where(({ i }) => - and(eq(i.projectId, p.id), eq(i.createdBy, p.createdBy)), - ) - .select(({ i }) => ({ - id: i.id, - title: i.title, - createdBy: i.createdBy, - })), + issues: include( + q + .from({ i: issuesWC }) + .where(({ i }) => + and(eq(i.projectId, p.id), eq(i.createdBy, p.createdBy)), + ) + .select(({ i }) => ({ + id: i.id, + title: i.title, + createdBy: i.createdBy, + })), + ), })), ) @@ -3269,14 +3339,16 @@ describe(`includes subqueries`, () => { q.from({ p: parents }).select(({ p }) => ({ id: p.id, createdBy: p.createdBy, - items: q - .from({ c: children }) - .where(({ c }) => eq(c.groupId, p.groupId)) - .where(({ c }) => eq(c.createdBy, p.createdBy)) - .select(({ c }) => ({ - id: c.id, - createdBy: c.createdBy, - })), + items: include( + q + .from({ c: children }) + .where(({ c }) => eq(c.groupId, p.groupId)) + .where(({ c }) => eq(c.createdBy, p.createdBy)) + .select(({ c }) => ({ + id: c.id, + createdBy: c.createdBy, + })), + ), })), ) @@ -3515,19 +3587,21 @@ describe(`includes subqueries`, () => { const collection = createLiveQueryCollection((q) => q.from({ parent: parents }).select(({ parent }) => ({ id: parent.id, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.groupId, parent.groupId)) - .where(({ child }) => gte(child.score, parent.minimumScore)) - .select(({ child }) => ({ - id: child.id, - notes: materialize( - q - .from({ note: notes }) - .where(({ note }) => eq(note.childId, child.id)) - .select(({ note }) => ({ id: note.id })), - ), - })), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.groupId, parent.groupId)) + .where(({ child }) => gte(child.score, parent.minimumScore)) + .select(({ child }) => ({ + id: child.id, + notes: materialize( + q + .from({ note: notes }) + .where(({ note }) => eq(note.childId, child.id)) + .select(({ note }) => ({ id: note.id })), + ), + })), + ), })), ) @@ -3605,16 +3679,18 @@ describe(`includes subqueries`, () => { q.from({ p: parents }).select(({ p }) => ({ id: p.id, createdBy: p.createdBy, - items: q - .from({ c: children }) - .where(({ c }) => eq(c.groupId, p.groupId)) - .where(({ c }) => eq(c.createdBy, p.createdBy)) - .orderBy(({ c }) => c.id, `asc`) - .limit(1) - .select(({ c }) => ({ - id: c.id, - createdBy: c.createdBy, - })), + items: include( + q + .from({ c: children }) + .where(({ c }) => eq(c.groupId, p.groupId)) + .where(({ c }) => eq(c.createdBy, p.createdBy)) + .orderBy(({ c }) => c.id, `asc`) + .limit(1) + .select(({ c }) => ({ + id: c.id, + createdBy: c.createdBy, + })), + ), })), ) @@ -3640,20 +3716,22 @@ describe(`includes subqueries`, () => { id: p.id, name: p.name, createdBy: p.createdBy, - issues: q - .from({ i: issuesWC }) - .where(({ i }) => - and( - eq(i.projectId, p.id), - eq(i.createdBy, p.createdBy), - eq(i.title, `Bug in Alpha`), - ), - ) - .select(({ i }) => ({ - id: i.id, - title: i.title, - createdBy: i.createdBy, - })), + issues: include( + q + .from({ i: issuesWC }) + .where(({ i }) => + and( + eq(i.projectId, p.id), + eq(i.createdBy, p.createdBy), + eq(i.title, `Bug in Alpha`), + ), + ) + .select(({ i }) => ({ + id: i.id, + title: i.title, + createdBy: i.createdBy, + })), + ), })), ) @@ -3716,24 +3794,28 @@ describe(`includes subqueries`, () => { const collection = createLiveQueryCollection((q) => q.from({ p: nProjects }).select(({ p }) => ({ id: p.id, - issues: q - .from({ i: nIssues }) - .where(({ i }) => eq(i.groupId, p.groupId)) - .where(({ i }) => eq(i.createdBy, p.createdBy)) - .select(({ i }) => ({ - id: i.id, - createdBy: i.createdBy, - categoryId: i.categoryId, - comments: q - .from({ c: nComments }) - .where(({ c }) => eq(c.categoryId, i.categoryId)) - .where(({ c }) => eq(c.createdBy, i.createdBy)) - .select(({ c }) => ({ - id: c.id, - createdBy: c.createdBy, - body: c.body, - })), - })), + issues: include( + q + .from({ i: nIssues }) + .where(({ i }) => eq(i.groupId, p.groupId)) + .where(({ i }) => eq(i.createdBy, p.createdBy)) + .select(({ i }) => ({ + id: i.id, + createdBy: i.createdBy, + categoryId: i.categoryId, + comments: include( + q + .from({ c: nComments }) + .where(({ c }) => eq(c.categoryId, i.categoryId)) + .where(({ c }) => eq(c.createdBy, i.createdBy)) + .select(({ c }) => ({ + id: c.id, + createdBy: c.createdBy, + body: c.body, + })), + ), + })), + ), })), ) @@ -3804,32 +3886,38 @@ describe(`includes subqueries`, () => { const collection = createLiveQueryCollection((q) => q.from({ a: l0 }).select(({ a }) => ({ id: a.id, - children: q - .from({ b: l1 }) - .where(({ b }) => eq(b.groupId, a.groupId)) - .where(({ b }) => eq(b.owner, a.owner)) - .select(({ b }) => ({ - id: b.id, - tagId: b.tagId, - owner: b.owner, - grandchildren: q - .from({ c: l2 }) - .where(({ c }) => eq(c.tagId, b.tagId)) - .where(({ c }) => eq(c.owner, b.owner)) - .select(({ c }) => ({ - id: c.id, - flagId: c.flagId, - owner: c.owner, - leaves: q - .from({ d: l3 }) - .where(({ d }) => eq(d.flagId, c.flagId)) - .where(({ d }) => eq(d.owner, c.owner)) - .select(({ d }) => ({ - id: d.id, - text: d.text, + children: include( + q + .from({ b: l1 }) + .where(({ b }) => eq(b.groupId, a.groupId)) + .where(({ b }) => eq(b.owner, a.owner)) + .select(({ b }) => ({ + id: b.id, + tagId: b.tagId, + owner: b.owner, + grandchildren: include( + q + .from({ c: l2 }) + .where(({ c }) => eq(c.tagId, b.tagId)) + .where(({ c }) => eq(c.owner, b.owner)) + .select(({ c }) => ({ + id: c.id, + flagId: c.flagId, + owner: c.owner, + leaves: include( + q + .from({ d: l3 }) + .where(({ d }) => eq(d.flagId, c.flagId)) + .where(({ d }) => eq(d.owner, c.owner)) + .select(({ d }) => ({ + id: d.id, + text: d.text, + })), + ), })), - })), - })), + ), + })), + ), })), ) @@ -3864,9 +3952,11 @@ describe(`includes subqueries`, () => { createLiveQueryCollection((q) => q.from({ p: projects }).select(({ p }) => ({ id: p.id, - issues: q - .from({ i: issues }) - .select(({ i }) => ({ id: i.id, title: i.title })), + issues: include( + q + .from({ i: issues }) + .select(({ i }) => ({ id: i.id, title: i.title })), + ), })), ), ).toThrow(/must have a WHERE clause with an eq\(\) condition/) @@ -3877,10 +3967,12 @@ describe(`includes subqueries`, () => { createLiveQueryCollection((q) => q.from({ p: projects }).select(({ p }) => ({ id: p.id, - issues: q - .from({ i: issues }) - .where(({ i }) => i.projectId) - .select(({ i }) => ({ id: i.id, title: i.title })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => i.projectId) + .select(({ i }) => ({ id: i.id, title: i.title })), + ), })), ), ).toThrow(/must have a WHERE clause with an eq\(\) condition/) @@ -3891,10 +3983,12 @@ describe(`includes subqueries`, () => { createLiveQueryCollection((q) => q.from({ p: projects }).select(({ p }) => ({ id: p.id, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, i.id)) - .select(({ i }) => ({ id: i.id, title: i.title })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, i.id)) + .select(({ i }) => ({ id: i.id, title: i.title })), + ), })), ), ).toThrow(/must have a WHERE clause with an eq\(\) condition/) @@ -3931,14 +4025,18 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ id: i.id, title: i.title })), - milestones: q - .from({ m: milestones }) - .where(({ m }) => eq(m.projectId, p.id)) - .select(({ m }) => ({ id: m.id, name: m.name })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ id: i.id, title: i.title })), + ), + milestones: include( + q + .from({ m: milestones }) + .where(({ m }) => eq(m.projectId, p.id)) + .select(({ m }) => ({ id: m.id, name: m.name })), + ), })), ) @@ -4029,14 +4127,18 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ id: i.id, title: i.title })), - milestones: q - .from({ m: milestones }) - .where(({ m }) => eq(m.projectId, p.id)) - .select(({ m }) => ({ id: m.id, name: m.name })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ id: i.id, title: i.title })), + ), + milestones: include( + q + .from({ m: milestones }) + .where(({ m }) => eq(m.projectId, p.id)) + .select(({ m }) => ({ id: m.id, name: m.name })), + ), })), ) @@ -4228,22 +4330,24 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - comments: toArray( - q - .from({ c: comments }) - .where(({ c }) => eq(c.issueId, i.id)) - .select(({ c }) => ({ - id: c.id, - body: c.body, - })), - ), - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + comments: toArray( + q + .from({ c: comments }) + .where(({ c }) => eq(c.issueId, i.id)) + .select(({ c }) => ({ + id: c.id, + body: c.body, + })), + ), + })), + ), })), ) } @@ -4461,13 +4565,15 @@ describe(`includes subqueries`, () => { .select(({ i }) => ({ id: i.id, title: i.title, - comments: q - .from({ c: comments }) - .where(({ c }) => eq(c.issueId, i.id)) - .select(({ c }) => ({ - id: c.id, - body: c.body, - })), + comments: include( + q + .from({ c: comments }) + .where(({ c }) => eq(c.issueId, i.id)) + .select(({ c }) => ({ + id: c.id, + body: c.body, + })), + ), })), ), })), @@ -4890,10 +4996,12 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issueCount: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ total: count(i.id) })), + issueCount: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ total: count(i.id) })), + ), })), ) } @@ -5010,17 +5118,21 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - commentCount: q - .from({ c: comments }) - .where(({ c }) => eq(c.issueId, i.id)) - .select(({ c }) => ({ total: count(c.id) })), - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + commentCount: include( + q + .from({ c: comments }) + .where(({ c }) => eq(c.issueId, i.id)) + .select(({ c }) => ({ total: count(c.id) })), + ), + })), + ), })), ) } @@ -5055,19 +5167,21 @@ describe(`includes subqueries`, () => { q.from({ p: projects }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q - .from({ i: issues }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - commentCount: toArray( - q - .from({ c: comments }) - .where(({ c }) => eq(c.issueId, i.id)) - .select(({ c }) => ({ total: count(c.id) })), - ), - })), + issues: include( + q + .from({ i: issues }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + commentCount: toArray( + q + .from({ c: comments }) + .where(({ c }) => eq(c.issueId, i.id)) + .select(({ c }) => ({ total: count(c.id) })), + ), + })), + ), })), ) } @@ -7203,17 +7317,21 @@ describe(`includes subqueries`, () => { q.from({ p: products }).select(({ p }) => ({ id: p.id, title: p.title, - priceRanges: q - .from({ pr: priceRanges }) - .where(({ pr }) => eq(pr.productId, p.id)) - .select(({ pr }) => ({ - id: pr.id, - regionId: pr.regionId, - region: q - .from({ r: regions }) - .where(({ r }) => eq(r.id, pr.regionId)) - .select(({ r }) => ({ id: r.id, name: r.name })), - })), + priceRanges: include( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + regionId: pr.regionId, + region: include( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ id: r.id, name: r.name })), + ), + })), + ), })), }) await collection.preload() @@ -7614,7 +7732,7 @@ describe(`includes subqueries`, () => { return { id: parent.id, - facade: childRows(), + facade: include(childRows()), array: toArray(childRows()), joined: concat( toArray( @@ -7883,3 +8001,44 @@ describe(`includes subqueries`, () => { }) }) }) + +describe(`a subquery in select needs an include operator`, () => { + const query = (shape: `field` | `nested` | `caseWhen`) => () => + createLiveQueryCollection((q) => + q.from({ p: createProjectsCollection() }).select(({ p }) => { + const issues = q + .from({ i: createIssuesCollection() }) + .where(({ i }) => eq(i.projectId, p.id)) as any + return shape === `field` + ? { id: p.id, issues } + : shape === `nested` + ? { id: p.id, detail: { issues } } + : { id: p.id, detail: caseWhen(gt(p.id, 0), { issues }, null) } + }), + ) + + it.each([`field`, `nested`, `caseWhen`] as const)( + `throws a coded error for a bare subquery: %s`, + (shape) => { + expect(query(shape)).toThrow(IncludeOperatorRequiredError) + expect(query(shape)).toThrow(/issues: include\(/) + }, + ) + + it(`includes a Collection with include()`, async () => { + const projects = createProjectsCollection() + const issues = createIssuesCollection() + const live = createLiveQueryCollection((q) => + q.from({ p: projects }).select(({ p }) => ({ + id: p.id, + issues: include( + q.from({ i: issues }).where(({ i }) => eq(i.projectId, p.id)), + ), + })), + ) + await live.preload() + const first = live.toArray[0]! + expect(first.issues.toArray.length).toBeGreaterThan(0) + await live.cleanup() + }) +}) diff --git a/packages/db/tests/query/ir-stable-identity-oracle.test.ts b/packages/db/tests/query/ir-stable-identity-oracle.test.ts index d422324ea4..fffa65e141 100644 --- a/packages/db/tests/query/ir-stable-identity-oracle.test.ts +++ b/packages/db/tests/query/ir-stable-identity-oracle.test.ts @@ -2,6 +2,9 @@ import { describe, expect, it, vi } from 'vitest' import { fc, test as fcTest } from '@fast-check/vitest' import { Temporal } from 'temporal-polyfill' import { innerJoin, leftJoin } from '@tanstack/db' +import { include } from '../../src/query/index.js' +import { materializeCompilation } from '../../src/query/live/materialized-pipeline.js' +import { includesRuntime } from '../../src/query/live/includes-runtime.js' import { processJoins } from '../../src/query/compiler/joins.js' import { applyOptimizations } from '../../src/query/optimizer-joins.js' import { CollectionImpl } from '../../src/collection/index.js' @@ -874,6 +877,7 @@ function createAlphaRenamedJoinQuery( { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `inner`, from: new CollectionRef( postsCollection as unknown as ConstructorParameters< @@ -918,6 +922,7 @@ function createAlphaRenamedImplicitJoinQuery( { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `inner`, from: new CollectionRef( postsCollection as unknown as ConstructorParameters< @@ -1022,6 +1027,7 @@ function createUnionDerivedIncludesQuery(parentAlias: string): QueryIR { new PropRef([parentAlias, `id`]), new PropRef([`userId`]), `posts`, + includesRuntime, undefined, undefined, `array`, @@ -1064,6 +1070,7 @@ function createCorrelatedUnionIncludesQuery(parentAlias: string): QueryIR { new PropRef([parentAlias, `id`]), new PropRef([`userId`]), `posts`, + includesRuntime, undefined, [new PropRef([parentAlias, `age`])], `array`, @@ -1104,6 +1111,7 @@ function createAlphaRenamedIncludesQuery( new PropRef([parentAlias, `id`]), new PropRef([childAlias, `userId`]), `posts`, + includesRuntime, [ new Func(`eq`, [ new PropRef([parentAlias, `status`]), @@ -1343,13 +1351,15 @@ const structuredQueries: Array<[string, () => QueryIR]> = [ getQueryIR( new Query().from({ user: usersCollection }).select(({ user }) => ({ id: user.id, - posts: new Query() - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)) - .select(({ post }) => ({ - id: post.id, - title: post.title, - })), + posts: include( + new Query() + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)) + .select(({ post }) => ({ + id: post.id, + title: post.title, + })), + ), })), ), ], diff --git a/packages/db/tests/query/optimizer-semantics-oracle.test.ts b/packages/db/tests/query/optimizer-semantics-oracle.test.ts index 6c66790837..842488417d 100644 --- a/packages/db/tests/query/optimizer-semantics-oracle.test.ts +++ b/packages/db/tests/query/optimizer-semantics-oracle.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest' +import { materializeCompilation } from '../../src/query/live/materialized-pipeline.js' import { applyOptimizations } from '../../src/query/optimizer-joins.js' import { processJoins } from '../../src/query/compiler/joins.js' import { @@ -512,6 +513,7 @@ describe(`optimizer residual convergence`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: join.type, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -578,6 +580,7 @@ describe(`optimizer residual convergence`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -588,6 +591,7 @@ describe(`optimizer residual convergence`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `inner`, from: new CollectionRef(collection, `tag`), on: new Func('eq', [ @@ -637,6 +641,7 @@ describe(`optimizer residual convergence`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: joinType, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -671,6 +676,7 @@ describe(`optimizer residual convergence`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -706,6 +712,7 @@ describe(`optimizer residual convergence`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -737,6 +744,7 @@ describe(`optimizer residual convergence`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -777,6 +785,7 @@ describe(`optimizer residual convergence`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -787,6 +796,7 @@ describe(`optimizer residual convergence`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: laterJoin, from: new CollectionRef(collection, `tag`), on: new Func('eq', [ @@ -850,6 +860,7 @@ describe(`optimizer residual convergence`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -891,6 +902,7 @@ describe(`optimizer residual convergence`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ diff --git a/packages/db/tests/query/optimizer.test.ts b/packages/db/tests/query/optimizer.test.ts index 1cde4243b6..a7c2cbb4b4 100644 --- a/packages/db/tests/query/optimizer.test.ts +++ b/packages/db/tests/query/optimizer.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest' +import { materializeCompilation } from '../../src/query/live/materialized-pipeline.js' import { grouping } from '../../src/query/compiler/grouping.js' import { applyOptimizations } from '../../src/query/optimizer-joins.js' import { processJoins } from '../../src/query/compiler/joins.js' @@ -139,6 +140,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -172,6 +174,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -216,6 +219,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new QueryRef(distinctSource, `p`), type: `inner`, on: createEq(createPropRef(`u`, `id`), createPropRef(`p`, `id`)), @@ -239,6 +243,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -289,6 +294,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -330,6 +336,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -369,6 +376,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -408,6 +416,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -453,6 +462,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -515,6 +525,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -551,6 +562,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -594,6 +606,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -633,6 +646,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -680,6 +694,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -710,6 +725,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -737,6 +753,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -794,6 +811,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -819,6 +837,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -856,6 +875,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -901,6 +921,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -951,6 +972,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1009,6 +1031,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1048,6 +1071,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new QueryRef( { from: new CollectionRef(mockCollection, `p`), @@ -1130,6 +1154,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1165,6 +1190,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1203,6 +1229,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq(createPropRef(`u`, `id`), createPropRef(`p`, `user_id`)), @@ -1231,6 +1258,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1289,6 +1317,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new QueryRef(sharedSubquery, `other_users`), // Same subquery object! type: `inner`, on: createEq( @@ -1351,6 +1380,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1396,6 +1426,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1435,6 +1466,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1485,6 +1517,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1522,6 +1555,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1572,6 +1606,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1626,6 +1661,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new QueryRef(unsafeSubquery, `dept_stats`), type: `inner`, on: createEq( @@ -1675,6 +1711,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1737,6 +1774,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1829,6 +1867,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `left`, from: new CollectionRef(teamMembersCollection, `teamMember`), on: createEq( @@ -1876,6 +1915,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `right`, from: new CollectionRef(profilesCollection, `profile`), on: createEq( @@ -1928,6 +1968,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `full`, from: new CollectionRef(paymentsCollection, `payment`), on: createEq( @@ -1975,6 +2016,7 @@ describe(`Query Optimizer`, () => { { compile: processJoins, optimize: applyOptimizations, + materialize: materializeCompilation, type: `inner`, from: new CollectionRef(departmentsCollection, `dept`), on: createEq( diff --git a/packages/db/tests/query/optional-fields-runtime.test.ts b/packages/db/tests/query/optional-fields-runtime.test.ts index f3fd3b082e..63f8376362 100644 --- a/packages/db/tests/query/optional-fields-runtime.test.ts +++ b/packages/db/tests/query/optional-fields-runtime.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from 'vitest' -import { createLiveQueryCollection, +import { + createLiveQueryCollection, eq, groupKeys, - leftJoin } from '../../src/query/index.js' + leftJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' diff --git a/packages/db/tests/query/public-container-copy.test.ts b/packages/db/tests/query/public-container-copy.test.ts index e29c5942bf..a6571aab0a 100644 --- a/packages/db/tests/query/public-container-copy.test.ts +++ b/packages/db/tests/query/public-container-copy.test.ts @@ -1,11 +1,10 @@ import { expect, it } from 'vitest' -import { transformPublicContainers } from '../../src/query/compiler/route-metadata.js' -import { - createLiveQueryCollection, +import { createLiveQueryCollection, eq, + include, materialize, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' +import { transformPublicContainers } from '../../src/query/compiler/route-metadata.js' import { createControlledCollection } from './includes-oracle-helpers.js' it.each( @@ -43,7 +42,7 @@ it.each( .select(({ inner }) => ({ id: inner.id })) return { id: parent.id, - collection: matches, + collection: include(matches), array: toArray(matches), materialized: materialize(matches), } diff --git a/packages/db/tests/query/replay-failure-boundary.test.ts b/packages/db/tests/query/replay-failure-boundary.test.ts index 54926265ca..fa2018640b 100644 --- a/packages/db/tests/query/replay-failure-boundary.test.ts +++ b/packages/db/tests/query/replay-failure-boundary.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest' +import { createLiveQueryCollection, eq, include } from '../../src/query/index.js' import { createCollection } from '../../src/collection' import { createDeferred } from '../../src/deferred' import { BasicIndex } from '../../src/indexes/basic-index' -import { createLiveQueryCollection, eq } from '../../src/query' import { PropRef } from '../../src/query/ir' import { evaluateReferenceExpression } from '../reference-expression-oracle' import { flushPromises } from '../utils' @@ -92,9 +92,11 @@ describe.each([`direct`, `query`] as const)( .select(({ row }) => ({ id: row.id, version: row.version, - children: q - .from({ child: children }) - .where(({ child }) => eq(child.id, row.id)), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.id, row.id)), + ), })), ) const live = consumer === `query` ? makeLive() : undefined diff --git a/packages/db/tests/query/scheduler.test.ts b/packages/db/tests/query/scheduler.test.ts index c84031269d..4fa8423d5f 100644 --- a/packages/db/tests/query/scheduler.test.ts +++ b/packages/db/tests/query/scheduler.test.ts @@ -1,12 +1,11 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { - createLiveQueryCollection, +import { createLiveQueryCollection, eq, fullJoin, + include, innerJoin, isNull, - leftJoin, -} from '../../src/query/index.js' + leftJoin } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { createTransaction } from '../../src/transactions.js' @@ -1640,12 +1639,16 @@ describe(`live query scheduler`, () => { query: (q) => q.from({ root: firstSource }).select(({ root }) => ({ id: root.id, - second: q - .from({ item: secondSource }) - .where(({ item }) => eq(item.id, root.id)), - third: q - .from({ item: thirdSource }) - .where(({ item }) => eq(item.id, root.id)), + second: include( + q + .from({ item: secondSource }) + .where(({ item }) => eq(item.id, root.id)), + ), + third: include( + q + .from({ item: thirdSource }) + .where(({ item }) => eq(item.id, root.id)), + ), })), }) type BuilderSyncConfig = Parameters< diff --git a/packages/db/tests/query/union-all.test.ts b/packages/db/tests/query/union-all.test.ts index 79066c4806..1ca1ff6bb0 100644 --- a/packages/db/tests/query/union-all.test.ts +++ b/packages/db/tests/query/union-all.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest' -import { - caseWhen, +import { caseWhen, coalesce, concat, count, @@ -8,11 +7,11 @@ import { eq, fullJoin, gt, + include, innerJoin, leftJoin, rightJoin, - toArray, -} from '../../src/query/index.js' + toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { @@ -1601,14 +1600,16 @@ describe(`unionAll`, () => { id: user.id, profile: caseWhen(user.id, { id: user.id, - items: q - .from({ item: events }) - .where(({ item }) => eq(item.userId, user.id)) - .orderBy(({ item }) => item.timestamp) - .select(({ item }) => ({ - label: item.label, - timestamp: item.timestamp, - })), + items: include( + q + .from({ item: events }) + .where(({ item }) => eq(item.userId, user.id)) + .orderBy(({ item }) => item.timestamp) + .select(({ item }) => ({ + label: item.label, + timestamp: item.timestamp, + })), + ), }), })) .orderBy(({ user }) => user.id) @@ -1710,23 +1711,25 @@ describe(`unionAll`, () => { key: run.key, order: run.order, status: run.status, - items: q - .from({ item: runItemsSource }) - .where(({ item }) => eq(item.runId, run.key)) - .orderBy(({ item }) => item.order) - .select(({ item }) => ({ - text: caseWhen(item.text.key, { - key: item.text.key, - runId: item.text.runId, - order: item.text.order, - status: item.text.status, - content: item.textContent, - }), - inactiveText: caseWhen(item.toolCall.key, { - content: item.textContent, - }), - toolCall: item.toolCall, - })), + items: include( + q + .from({ item: runItemsSource }) + .where(({ item }) => eq(item.runId, run.key)) + .orderBy(({ item }) => item.order) + .select(({ item }) => ({ + text: caseWhen(item.text.key, { + key: item.text.key, + runId: item.text.runId, + order: item.text.order, + status: item.text.status, + content: item.textContent, + }), + inactiveText: caseWhen(item.toolCall.key, { + content: item.textContent, + }), + toolCall: item.toolCall, + })), + ), }), })) }) From 9ffd904e3a83a3cd3323092272c58ac64a7f8125 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 16:06:56 -0600 Subject: [PATCH 05/32] feat(db): make live-query pooling opt-in with pooledLiveQueries Hooks pool a query only when its config passes pool: pooledLiveQueries. The value carries the pooled view and its identity shortcut, so apps that do not pool do not bundle them. React Suspense and DbClient queries pass pool: false and never pool. Co-authored-by: Isaac --- packages/db/src/live-query-options.ts | 39 +++++++---- packages/db/src/query/index.ts | 6 ++ packages/db/src/query/live/types.ts | 9 +++ packages/db/src/query/pooled-live-query.ts | 9 +++ .../db-client-preload-identity-oracle.test.ts | 29 ++++++--- .../tests/initial-readiness-exposure.test.ts | 7 +- packages/db/tests/live-query-options.test.ts | 64 +++++++++++++++---- .../query/pooled-live-query-gc-oracle.test.ts | 15 +++-- .../tests/query/pooled-query-identity.test.ts | 3 +- packages/react-db/src/useLiveQuery.ts | 5 +- packages/svelte-db/src/useLiveQuery.svelte.ts | 4 +- 11 files changed, 144 insertions(+), 46 deletions(-) diff --git a/packages/db/src/live-query-options.ts b/packages/db/src/live-query-options.ts index 25abc0d25c..bdfc57a28a 100644 --- a/packages/db/src/live-query-options.ts +++ b/packages/db/src/live-query-options.ts @@ -2,10 +2,6 @@ import { BaseQueryBuilder } from './query/builder/index.js' import { collectSourceRefs, requireCollectionSource } from './query/ir.js' import { isCollection } from './live-query-adapter.js' import { createLiveQueryCollection } from './query/live-query-collection.js' -import { - createPooledLiveQuery, - getPooledQueryIdentity, -} from './query/pooled-live-query.js' import { getStableQueryBuilderHash, getStableValueHash, @@ -15,6 +11,7 @@ import { codedMessage, devBuild } from './error-message.js' import type { Collection, CollectionImpl } from './collection/index.js' import type { CollectionOptionsIdentity } from './collection-options.js' import type { CollectionOptions, DbClient } from './client.js' +import type { PooledLiveQueries } from './query/pooled-live-query.js' import type { Context, InitialQueryBuilder, @@ -162,12 +159,15 @@ export function getPreparedLiveQuerySources( return isCollection(query) ? [query] : [] } -export function getPreparedLiveQueryIdentity(value: unknown): unknown { +export function getPreparedLiveQueryIdentity( + value: unknown, + pool?: PooledLiveQueries, +): unknown { if (isCollection(value)) return [`collection`, value.id] if (value instanceof BaseQueryBuilder) { // A pooled query's fields and literals identify its rows without // canonicalizing its whole IR on every render. - const pooled = getPooledQueryIdentity(value) + const pooled = pool?.getPooledQueryIdentity(value) return pooled === undefined ? [`query`, getStableQueryBuilderHash(value)] : [`pooled`, pooled] @@ -176,7 +176,7 @@ export function getPreparedLiveQueryIdentity(value: unknown): unknown { const config = value as LiveQueryCollectionConfig return [ `config`, - getPreparedLiveQueryIdentity(config.query), + getPreparedLiveQueryIdentity(config.query, config.pool), [`getKey`, config.getKey], [`schema`, config.schema], [`singleResult`, config.singleResult === true], @@ -214,14 +214,17 @@ export function getLiveQueryHash( function poolConfig( config: LiveQueryCollectionConfig, gcTime: number | undefined, + pool: PooledLiveQueries, ): Collection | undefined { if ( !(config.query instanceof BaseQueryBuilder) || - !Object.keys(config).every((key) => key === `query` || key === `gcTime`) + !Object.keys(config).every( + (key) => key === `query` || key === `gcTime` || key === `pool`, + ) ) { return undefined } - return createPooledLiveQuery(config.query, { + return pool.createPooledLiveQuery(config.query, { gcTime: config.gcTime ?? gcTime, }) } @@ -230,9 +233,15 @@ export function resolveLiveQueryValue( value: unknown, { gcTime, - pool = true, + pool, startSync = true, - }: { gcTime?: number; pool?: boolean; startSync?: boolean } = {}, + }: { + gcTime?: number + // `false` keeps a hook that cannot pool, such as a Suspense or DbClient + // query, from pooling even when its config passes `pooledLiveQueries`. + pool?: PooledLiveQueries | false + startSync?: boolean + } = {}, ): Collection | null { if (value === undefined || value === null) return null if (isCollection(value)) { @@ -241,14 +250,16 @@ export function resolveLiveQueryValue( } if (value instanceof BaseQueryBuilder) { return ( - (pool ? createPooledLiveQuery(value, { gcTime }) : undefined) ?? + (pool ? pool.createPooledLiveQuery(value, { gcTime }) : undefined) ?? createLiveQueryCollection({ query: value, startSync, gcTime }) ) } if (typeof value === `object`) { - const config = value as LiveQueryCollectionConfig + const { pool: configPool, ...config } = + value as LiveQueryCollectionConfig + const pooling = pool === false ? undefined : (configPool ?? pool) return ( - (pool ? poolConfig(config, gcTime) : undefined) ?? + (pooling ? poolConfig(config, gcTime, pooling) : undefined) ?? createLiveQueryCollection({ ...config, startSync: config.startSync ?? startSync, diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index f37f594dde..3403200014 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -35,6 +35,12 @@ export { type Prettify, } from './builder/index.js' +// Pooled live queries for framework hooks +export { + pooledLiveQueries, + type PooledLiveQueries, +} from './pooled-live-query.js' + // Grouping without aggregates export { groupKeys, type GroupKeys } from './builder/group-keys.js' diff --git a/packages/db/src/query/live/types.ts b/packages/db/src/query/live/types.ts index 307397698e..6e12748736 100644 --- a/packages/db/src/query/live/types.ts +++ b/packages/db/src/query/live/types.ts @@ -1,4 +1,5 @@ import type { D2, RootStreamBuilder } from '@tanstack/db-ivm' +import type { PooledLiveQueries } from '../pooled-live-query.js' import type { CollectionConfig, ResultStream, @@ -72,6 +73,14 @@ export interface LiveQueryCollectionConfig< */ id?: string + /** + * Framework hooks only: pass `pooledLiveQueries` to serve this query from a + * shared equality partition when its shape allows. A config with any option + * other than `query`, `gcTime`, and `pool` compiles its own Collection. + * `createLiveQueryCollection` does not pool. + */ + pool?: PooledLiveQueries + /** * Query builder function that defines the live query */ diff --git a/packages/db/src/query/pooled-live-query.ts b/packages/db/src/query/pooled-live-query.ts index 9f87605ff3..0715bac76d 100644 --- a/packages/db/src/query/pooled-live-query.ts +++ b/packages/db/src/query/pooled-live-query.ts @@ -724,3 +724,12 @@ export function createPooledLiveQuery( : undefined, ) as unknown as Collection } + +/** + * Pooled live queries for framework hooks. Pass it as `pool` in a hook's + * query config to serve equality-filtered queries of one shape from a shared + * partition. Apps that do not pass it do not bundle pooling. + */ +export const pooledLiveQueries = { createPooledLiveQuery, getPooledQueryIdentity } + +export type PooledLiveQueries = typeof pooledLiveQueries diff --git a/packages/db/tests/db-client-preload-identity-oracle.test.ts b/packages/db/tests/db-client-preload-identity-oracle.test.ts index baa25a4eae..89cb3634e7 100644 --- a/packages/db/tests/db-client-preload-identity-oracle.test.ts +++ b/packages/db/tests/db-client-preload-identity-oracle.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, it, vi } from 'vitest' +import { pooledLiveQueries } from '../src/query/pooled-live-query.js' /** * # A DbClient preload cannot substitute another source Collection * @@ -55,7 +57,6 @@ * the observer's own row after preload. It does not model concurrent first * claims or arbitrary hydration orders. */ -import { describe, expect, it, vi } from 'vitest' import { DbClient, Query, @@ -155,7 +156,9 @@ describe(`DbClient preload source identity`, () => { /different source Collections/, ) if (streamState === `pending`) { - conflictingView = resolveLiveQueryValue(otherQuery) + conflictingView = resolveLiveQueryValue(otherQuery, { + pool: pooledLiveQueries, + }) expect(conflictingView).not.toBeNull() conflictingObserver = createLiveQueryObserver(conflictingView, { client: browser, @@ -279,7 +282,9 @@ describe(`DbClient preload source identity`, () => { if (streamState === `settled`) expect(rows(client)).toEqual([{ id: `one`, value: `server` }]) - view = resolveLiveQueryValue(replacementQuery) + view = resolveLiveQueryValue(replacementQuery, { + pool: pooledLiveQueries, + }) expect(view).not.toBeNull() observer = createLiveQueryObserver(view, { client, queryHash }) expect(() => { @@ -450,7 +455,9 @@ describe(`DbClient preload source identity`, () => { await client.preloadLiveQuery({ query: firstQuery }) const queryHash = getLiveQueryHash({ query: firstQuery }) expect(getLiveQueryHash({ query: candidateQuery })).toBe(queryHash) - view = resolveLiveQueryValue(candidateQuery) + view = resolveLiveQueryValue(candidateQuery, { + pool: pooledLiveQueries, + }) expect(view).not.toBeNull() observer = createLiveQueryObserver(view, { client, queryHash }) @@ -519,7 +526,9 @@ describe(`DbClient preload source identity`, () => { expect(getLiveQueryHash({ query: candidateQuery })).toBe(queryHash) expect(rows(client)).toEqual([{ id: `one`, value: `first` }]) - firstView = resolveLiveQueryValue(firstQuery) + firstView = resolveLiveQueryValue(firstQuery, { + pool: pooledLiveQueries, + }) expect(firstView).not.toBeNull() firstObserver = createLiveQueryObserver(firstView, { client, @@ -536,7 +545,9 @@ describe(`DbClient preload source identity`, () => { ), ).toEqual([`first`]) - nextView = resolveLiveQueryValue(candidateQuery) + nextView = resolveLiveQueryValue(candidateQuery, { + pool: pooledLiveQueries, + }) expect(nextView).not.toBeNull() nextObserver = createLiveQueryObserver(nextView, { client, queryHash }) expect(() => { @@ -585,7 +596,9 @@ describe(`DbClient preload source identity`, () => { await client.preloadLiveQuery({ query: firstQuery }) expect(rows(client)?.[0]?.value).toBe(`first`) - const view = resolveLiveQueryValue(candidateQuery) + const view = resolveLiveQueryValue(candidateQuery, { + pool: pooledLiveQueries, + }) expect(view).not.toBeNull() expect(view?.config, `the receiving view is pooled`).toBeUndefined() observer = createLiveQueryObserver(view, { client, queryHash }) @@ -629,7 +642,7 @@ describe(`DbClient preload source identity`, () => { await client.preloadLiveQuery({ query: firstQuery }) const queryHash = getLiveQueryHash({ query: firstQuery }) expect(getLiveQueryHash({ query: secondQuery })).toBe(queryHash) - view = resolveLiveQueryValue(secondQuery) + view = resolveLiveQueryValue(secondQuery, { pool: pooledLiveQueries }) expect(view).not.toBeNull() observer = createLiveQueryObserver(view, { client, queryHash }) let preload: Promise | undefined diff --git a/packages/db/tests/initial-readiness-exposure.test.ts b/packages/db/tests/initial-readiness-exposure.test.ts index 97bd384537..8f25f79568 100644 --- a/packages/db/tests/initial-readiness-exposure.test.ts +++ b/packages/db/tests/initial-readiness-exposure.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest' +import { pooledLiveQueries } from '../src/query/pooled-live-query.js' /** * Initial-query readiness exposure witnesses for core readers. * @@ -12,7 +14,6 @@ * live-query path that frameworks use for `eq` filters, and * `toArrayWhenReady`. */ -import { describe, expect, it } from 'vitest' import { createCollection } from '../src/collection/index.js' import { createLiveQueryCollection, eq } from '../src/query/index.js' import { resolveLiveQueryValue } from '../src/live-query-options.js' @@ -99,7 +100,7 @@ describe(`core readers release at initial-query readiness`, () => { const query = new Query() .from({ post: source.collection }) .where(({ post }) => eq(post.id, `p1`)) - const live = resolveLiveQueryValue(query)! + const live = resolveLiveQueryValue(query, { pool: pooledLiveQueries })! // On-demand loading needs the live-query Collection, not the pooled view. expect(`materialize` in live).toBe(false) const subscription = live.subscribeChanges(() => {}) @@ -130,7 +131,7 @@ describe(`core readers release at initial-query readiness`, () => { const query = new Query() .from({ post: source.collection }) .where(({ post }) => eq(post.id, `p1`)) - const live = resolveLiveQueryValue(query)! + const live = resolveLiveQueryValue(query, { pool: pooledLiveQueries })! expect(`materialize` in live).toBe(true) const subscription = live.subscribeChanges(() => {}) await flush() diff --git a/packages/db/tests/live-query-options.test.ts b/packages/db/tests/live-query-options.test.ts index 587ab0d23d..d6958b1afd 100644 --- a/packages/db/tests/live-query-options.test.ts +++ b/packages/db/tests/live-query-options.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { leftJoin } from '@tanstack/db' -import { include } from '../src/query/index.js' +import { include, pooledLiveQueries } from '../src/query/index.js' import { createCollection } from '../src/collection/index.js' import { DbClient, collectionOptions } from '../src/client.js' import { @@ -11,10 +11,7 @@ import { resolveLiveQueryValue, } from '../src/live-query-options.js' import { BaseQueryBuilder, getQueryIR } from '../src/query/builder/index.js' -import { - caseWhen, - eq, -} from '../src/query/builder/functions.js' +import { caseWhen, eq } from '../src/query/builder/functions.js' import { materialize, toArray } from '../src/query/builder/include-functions.js' import { collectSourceRefs } from '../src/query/ir.js' import type { DeferredLiveQueryCollections } from '../src/live-query-options.js' @@ -126,8 +123,11 @@ describe(`live query preparation`, () => { for (const collection of renderDeferrals) collection._resumeSyncStart() renderDeferrals.clear() - const continued = prepared.join(leftJoin({ later: laterDescriptor }, - ({ source, later }) => eq(source.id, later.id))) + const continued = prepared.join( + leftJoin({ later: laterDescriptor }, ({ source, later }) => + eq(source.id, later.id), + ), + ) expect( collectSourceRefs(getQueryIR(continued)).map((ref) => ref.type), ).toEqual([`collectionRef`, `descriptorRef`]) @@ -168,8 +168,11 @@ describe(`live query preparation`, () => { client, deferrals, ) as typeof base - const continued = prepared.join(leftJoin({ later: laterDescriptor }, - ({ source, later }) => eq(source.id, later.id))) + const continued = prepared.join( + leftJoin({ later: laterDescriptor }, ({ source, later }) => + eq(source.id, later.id), + ), + ) expect(starts).toBe(0) const consumed = prepareLiveQueryValue( @@ -212,8 +215,11 @@ describe(`live query preparation`, () => { new Set(), ) - const continued = captured!.join(leftJoin({ later: laterDescriptor }, - ({ source, later }) => eq(source.id, later.id))) + const continued = captured!.join( + leftJoin({ later: laterDescriptor }, ({ source, later }) => + eq(source.id, later.id), + ), + ) expect( collectSourceRefs(getQueryIR(continued)).map((ref) => ref.type), ).toEqual([`descriptorRef`, `descriptorRef`]) @@ -431,3 +437,39 @@ describe(`live query identity`, () => { ) }) }) + +describe(`pooling is opt-in`, () => { + const source = () => + createCollection<{ id: number; group: number }>({ + id: `pool-opt-in-${Math.random()}`, + getKey: (row) => row.id, + sync: { sync: ({ markReady }) => markReady() }, + }) + const eqQuery = (rows: ReturnType) => + new BaseQueryBuilder() + .from({ row: rows }) + .where(({ row }) => eq(row.group, 1)) + + // A pooled view has no Collection config; a compiled live query has one. + it(`compiles its own Collection unless the hook passes pooledLiveQueries`, () => { + const rows = source() + expect(resolveLiveQueryValue(eqQuery(rows))?.config).toBeDefined() + expect( + resolveLiveQueryValue({ query: eqQuery(rows) })?.config, + ).toBeDefined() + expect( + resolveLiveQueryValue({ query: eqQuery(rows), pool: pooledLiveQueries }) + ?.config, + ).toBeUndefined() + }) + + it(`does not pool when the hook passes pool: false`, () => { + const rows = source() + expect( + resolveLiveQueryValue( + { query: eqQuery(rows), pool: pooledLiveQueries }, + { pool: false }, + )?.config, + ).toBeDefined() + }) +}) diff --git a/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts b/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts index 583c5ef24f..aeb7506588 100644 --- a/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts +++ b/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts @@ -1,9 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createPooledLiveQuery, pooledLiveQueries } from '../../src/query/pooled-live-query.js' import { CollectionImpl, createCollection } from '../../src/collection/index.js' import { createLiveQueryObserver } from '../../src/live-query-observer.js' import { Query } from '../../src/query/builder/index.js' import { createLiveQueryCollection, eq, not } from '../../src/query/index.js' -import { createPooledLiveQuery } from '../../src/query/pooled-live-query.js' import { resolveLiveQueryValue } from '../../src/live-query-options.js' import { mockSyncCollectionOptions } from '../utils.js' import type { Collection } from '../../src/collection/index.js' @@ -248,7 +248,9 @@ describe(`pooled live query gcTime`, () => { // A pooled view and a compiled one, each on its own source. const pooledOnOwn = makeSource() const pooledObserver = createLiveQueryObserver( - resolveLiveQueryValue(query(pooledOnOwn)(new Query())), + resolveLiveQueryValue(query(pooledOnOwn)(new Query()), { + pool: pooledLiveQueries, + }), { mode: `wholesale` }, ) pooledObserver.subscribe(() => {})() @@ -272,9 +274,12 @@ describe(`pooled live query gcTime`, () => { it(`pools a query config that names only its query`, () => { const source = makeSource() const observe = (value: unknown) => - createLiveQueryObserver(resolveLiveQueryValue(value, { gcTime: 1 }), { - mode: `wholesale`, - }).subscribe(() => {}) + createLiveQueryObserver( + resolveLiveQueryValue(value, { pool: pooledLiveQueries, gcTime: 1 }), + { + mode: `wholesale`, + }, + ).subscribe(() => {}) const stops = [ observe({ query: query(source)(new Query()) }), observe({ query: query(source)(new Query()), gcTime: 5 }), diff --git a/packages/db/tests/query/pooled-query-identity.test.ts b/packages/db/tests/query/pooled-query-identity.test.ts index aab60ccf61..53465ec079 100644 --- a/packages/db/tests/query/pooled-query-identity.test.ts +++ b/packages/db/tests/query/pooled-query-identity.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { pooledLiveQueries } from '../../src/query/pooled-live-query.js' import { createCollection } from '../../src/collection/index.js' import { getPreparedLiveQueryIdentity } from '../../src/live-query-options.js' import { Query } from '../../src/query/builder/index.js' @@ -23,7 +24,7 @@ const makeSource = () => }), ) const identity = (build: (q: any) => unknown) => - getPreparedLiveQueryIdentity(build(new Query())) + getPreparedLiveQueryIdentity(build(new Query()), pooledLiveQueries) describe(`pooled query identity`, () => { const source = makeSource() diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index 853d4ea68b..0b357b1d00 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -955,8 +955,9 @@ function useLiveQueryImpl( gcTime: forSuspense ? DEFAULT_SUSPENSE_GC_TIME_MS : DEFAULT_GC_TIME_MS, - // Hydration and Suspense key the live-query Collection by identity. - pool: !forSuspense && !dbClient, + // Hydration and Suspense key the live-query Collection by identity, + // so they never pool, even when the config passes a pool. + pool: forSuspense || dbClient ? false : undefined, // Without a DbClient, Suspense starts it through a preload below. startSync: !forSuspense || dbClient !== undefined, }) as SuspenseCollection | null diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index 10c8967112..ae062cbbac 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -428,12 +428,12 @@ export function useLiveQuery( // Hydration keys the live-query Collection by identity. collection = resolveLiveQueryValue(preparedValue, { gcTime: DEFAULT_GC_TIME_MS, - pool: !dbClient, + pool: dbClient ? false : undefined, }) } else { collection = resolveLiveQueryValue(preparedValue, { gcTime: DEFAULT_GC_TIME_MS, - pool: !dbClient, + pool: dbClient ? false : undefined, }) } From 318ffda17cbad53df62b18a1ecbf824bc219101f Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 16:26:26 -0600 Subject: [PATCH 06/32] test: migrate packages to opt-in operators and pooling Join calls use operator values, bare includes use include(), and the conformance pooling scenario passes pooledLiveQueries through each driver's config form. A pooled config with a query function builds it once. Co-authored-by: Isaac --- packages/angular-db/tests/conformance.test.ts | 6 +- .../src/suites/joins-oracle.suite.ts | 74 ++++++++----------- .../tests/persisted-readiness-oracle.test.ts | 19 +++-- packages/db/src/live-query-options.ts | 10 ++- packages/db/tests/conformance/contract.ts | 11 +-- packages/db/tests/conformance/suite.ts | 60 ++++++++++----- .../includes-work-counter-oracle.test.ts | 19 +++-- .../load-subset-lifecycle-oracle.test.ts | 12 ++- .../query-db-collection/tests/query.test.ts | 33 ++++----- .../refetch-ordered-prefix-oracle.test.ts | 14 ++-- packages/react-db/tests/conformance.test.tsx | 6 +- .../descriptor-query-binding-oracle.test.tsx | 40 +++++----- .../tests/source-id-reuse-oracle.test.tsx | 43 +++++------ packages/react-db/tests/useLiveQuery.test.tsx | 42 +++++------ packages/solid-db/tests/conformance.test.tsx | 6 +- packages/solid-db/tests/useLiveQuery.test.tsx | 37 ++++------ .../tests/conformance.svelte.test.ts | 6 +- .../tests/useLiveQuery.svelte.test.ts | 27 +++---- packages/vue-db/tests/conformance.test.ts | 6 +- packages/vue-db/tests/useLiveQuery.test.ts | 27 +++---- 20 files changed, 248 insertions(+), 250 deletions(-) diff --git a/packages/angular-db/tests/conformance.test.ts b/packages/angular-db/tests/conformance.test.ts index 5d13b707d4..e374b55157 100644 --- a/packages/angular-db/tests/conformance.test.ts +++ b/packages/angular-db/tests/conformance.test.ts @@ -40,6 +40,7 @@ import { runSuite } from '../../db/tests/conformance/suite' import { makeLaggingSource } from '../../db/tests/conformance/lagging-source' import { expectResultSurface } from '../../db/tests/conformance/result-laws' import { withScopeSetup } from '../../db/tests/conformance/scope-setup' +import type { PooledLiveQueries } from '@tanstack/db' import type { ConformanceResult, ControllableHandle, @@ -196,7 +197,10 @@ function mountCollection(collection: any) { return makeHandle(result, destroy) } -function mountConfig(build: QueryBuild, opts?: { gcTime?: number }) { +function mountConfig( + build: QueryBuild, + opts?: { gcTime?: number; pool?: PooledLiveQueries }, +) { const { result, destroy } = inCtx(() => injectLiveQuery({ query: build, ...opts }), ) diff --git a/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts b/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts index 004ea4acf7..91fa8d1f43 100644 --- a/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts +++ b/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts @@ -1,12 +1,12 @@ +import { randomUUID } from 'node:crypto' +import { createLiveQueryCollection, eq, gt, isNull, leftJoin } from '@tanstack/db' /** * Joins Test Suite * * Tests multi-collection joins with various syncMode combinations */ -import { randomUUID } from 'node:crypto' import { describe, expect, it, vi } from 'vitest' -import { createLiveQueryCollection, eq, gt, isNull } from '@tanstack/db' import { waitFor, waitForQueryData } from '../utils/helpers' import type { Comment, @@ -382,9 +382,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId))) .select(({ user, post }) => ({ id: post.id, userName: user.name, @@ -435,9 +434,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { q .from({ user: usersCollection }) .where(({ user }) => eq(user.isActive, true)) - .join({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId))) .where(({ post }) => gt(post.viewCount, 10)) .select(({ user, post }) => ({ id: post.id, @@ -486,9 +484,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersEager }) - .join({ post: postsOnDemand }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: postsOnDemand }, ({ user, post }) => + eq(user.id, post.userId))) .select(({ user, post }) => ({ id: post.id, userName: user.name, @@ -536,9 +533,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId))) .orderBy(({ post }) => post.viewCount, `desc`) .select(({ user, post }) => ({ id: post.id, @@ -606,9 +602,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId))) .orderBy(({ post }) => post.id, `asc`) .limit(10) .offset(5) @@ -654,12 +649,10 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ) - .join({ comment: comments }, ({ post, comment }) => - eq(post.id, comment.postId), - ) + .join(leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId))) + .join(leftJoin({ comment: comments }, ({ post, comment }) => + eq(post.id, comment.postId))) .select(({ user, post, comment }) => ({ id: comment.id, userName: user.name, @@ -712,13 +705,11 @@ export function createJoinsTestSuite(getConfig: () => Promise) { q .from({ user: users }) .where(({ user }) => eq(user.isActive, true)) - .join({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId))) .where(({ post }) => isNull(post.deletedAt)) - .join({ comment: comments }, ({ post, comment }) => - eq(post.id, comment.postId), - ) + .join(leftJoin({ comment: comments }, ({ post, comment }) => + eq(post.id, comment.postId))) .where(({ comment }) => isNull(comment.deletedAt)) .select(({ user, post, comment }) => ({ id: comment.id, @@ -769,12 +760,10 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersEager }) - .join({ post: postsOnDemand }, ({ user, post }) => - eq(user.id, post.userId), - ) - .join({ comment: commentsOnDemand }, ({ post, comment }) => - eq(post.id, comment.postId), - ) + .join(leftJoin({ post: postsOnDemand }, ({ user, post }) => + eq(user.id, post.userId))) + .join(leftJoin({ comment: commentsOnDemand }, ({ post, comment }) => + eq(post.id, comment.postId))) .select(({ user, post, comment }) => ({ id: comment.id, userName: user.name, @@ -822,9 +811,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId))) .where(({ post }) => gt(post.viewCount, 50)) .select(({ user, post }) => ({ id: post.id, @@ -871,9 +859,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { q .from({ user: users }) .where(({ user }) => gt(user.age, 30)) - .join({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId))) .select(({ user, post }) => ({ id: post.id, userName: user.name, @@ -919,9 +906,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId))) .select(({ user, post }) => ({ id: user.id, userName: user.name, diff --git a/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts b/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts index 19157c9c2e..c95a929c08 100644 --- a/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts +++ b/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts @@ -1,14 +1,13 @@ -import { describe, expect, it, vi } from 'vitest' -import { - DbClient, +import { DbClient, PERSISTED_READINESS, createCollection, createLiveQueryCollection, createLiveQueryObserver, eq, getPersistedReadinessSource, - waitForPersistedInitialRender, -} from '@tanstack/db' + leftJoin, + waitForPersistedInitialRender } from '@tanstack/db' +import { describe, expect, it, vi } from 'vitest' import { persistedCollectionOptions } from '../src/index.js' import type { PersistenceAdapter } from '../src/index.js' @@ -263,7 +262,7 @@ describe(`persisted-readiness oracle`, () => { query: (q) => q .from({ left }) - .join({ right }, ({ left: a, right: b }) => eq(a.id, b.id)), + .join(leftJoin({ right }, ({ left: a, right: b }) => eq(a.id, b.id))), }) const observer = createLiveQueryObserver(query) await checkWithCleanup(() => { @@ -342,7 +341,7 @@ describe(`persisted-readiness oracle`, () => { query: (q) => q .from({ a: sourceA }) - .join({ b: sourceB }, ({ a, b }) => eq(a.id, b.id)), + .join(leftJoin({ b: sourceB }, ({ a, b }) => eq(a.id, b.id))), }) const observer = createLiveQueryObserver(query) const unsubscribe = observer.subscribe(() => {}) @@ -403,7 +402,7 @@ describe(`persisted-readiness oracle`, () => { query: (q) => q .from({ a: sourceA }) - .join({ b: sourceB }, ({ a, b }) => eq(a.id, b.id)), + .join(leftJoin({ b: sourceB }, ({ a, b }) => eq(a.id, b.id))), }) const observer = createLiveQueryObserver(query) const unsubscribe = observer.subscribe(() => {}) @@ -666,7 +665,7 @@ describe(`persisted-readiness oracle`, () => { query: (q) => q .from({ a: sourceA }) - .join({ b: sourceB }, ({ a, b }) => eq(a.id, b.id)), + .join(leftJoin({ b: sourceB }, ({ a, b }) => eq(a.id, b.id))), }) observer = createLiveQueryObserver(query) unsubscribe = observer.subscribe(() => {}) @@ -1145,7 +1144,7 @@ describe(`persisted-readiness oracle`, () => { query: (q) => q .from({ a: sourceA }) - .join({ b: sourceB }, ({ a, b }) => eq(a.id, b.id)), + .join(leftJoin({ b: sourceB }, ({ a, b }) => eq(a.id, b.id))), }) const observer = createLiveQueryObserver(query) const unsubscribe = observer.subscribe(() => {}) diff --git a/packages/db/src/live-query-options.ts b/packages/db/src/live-query-options.ts index bdfc57a28a..6351e8a2a4 100644 --- a/packages/db/src/live-query-options.ts +++ b/packages/db/src/live-query-options.ts @@ -255,9 +255,17 @@ export function resolveLiveQueryValue( ) } if (typeof value === `object`) { - const { pool: configPool, ...config } = + const { pool: configPool, ...given } = value as LiveQueryCollectionConfig const pooling = pool === false ? undefined : (configPool ?? pool) + // A pooled view needs the built query. Build a query function once and + // compile the same builder if the partition cannot serve it. + const built = + pooling && typeof given.query === `function` + ? given.query(createInitialQueryBuilder()) + : undefined + const config = + built instanceof BaseQueryBuilder ? { ...given, query: built } : given return ( (pooling ? poolConfig(config, gcTime, pooling) : undefined) ?? createLiveQueryCollection({ diff --git a/packages/db/tests/conformance/contract.ts b/packages/db/tests/conformance/contract.ts index 92075b4187..bffb9e87ca 100644 --- a/packages/db/tests/conformance/contract.ts +++ b/packages/db/tests/conformance/contract.ts @@ -22,7 +22,7 @@ * All current scenarios must pass. A future known bug needs an independently * approved exact failure signature; a whole-test waiver could hide another bug. */ -import type { Collection } from '@tanstack/db' +import type { Collection, PooledLiveQueries } from '@tanstack/db' /** Default row shape used by the base scenarios (a "person"). */ export interface Row { @@ -194,10 +194,10 @@ export interface LiveQueryDriver { ) => ControllableHandle

/** Mount a pre-created collection passed directly to the hook. */ mountCollection: (collection: Collection) => LiveQueryHandle - /** Mount via the config-object input form (`{ query: build }`). */ + /** Mount via the config-object input form (`{ query: build, ...opts }`). */ mountConfig: ( build: QueryBuild, - opts?: { gcTime?: number }, + opts?: { gcTime?: number; pool?: PooledLiveQueries }, ) => LiveQueryHandle /** Mount an explicitly-disabled query (adapter's own null/undefined form). */ mountDisabled: () => LiveQueryHandle @@ -214,8 +214,9 @@ export interface LiveQueryDriver { serverSnapshot?: boolean suspense?: boolean /** - * Public sharing policy: queries filtered only by `eq` on one source share - * one source subscription per filtered field set, instead of one each. + * Public sharing policy: queries whose config passes `pooledLiveQueries` + * and that filter only by `eq` on one source share one source subscription + * per filtered field set, instead of one each. */ pooledEqFilters?: boolean } diff --git a/packages/db/tests/conformance/suite.ts b/packages/db/tests/conformance/suite.ts index 2b6bf40d3f..38df7e0693 100644 --- a/packages/db/tests/conformance/suite.ts +++ b/packages/db/tests/conformance/suite.ts @@ -37,8 +37,12 @@ import { describe, expect, it } from 'vitest' * no-data, initial readiness over a lagging on-demand source, param recompilation, optimistic mutation, pre-created & config-object * inputs, error status, and order-only moves. */ -import { leftJoin, waitForPersistedInitialRender } from '@tanstack/db' -import { include } from '../../src/query/index.js' +import { + include, + leftJoin, + pooledLiveQueries, + waitForPersistedInitialRender, +} from '@tanstack/db' import { ScenarioLifetime } from './scenario-lifetime' import { ScenarioSources } from './scenario-sources' import { scenarioRegistry } from './registration' @@ -207,8 +211,12 @@ export function runSuite(rawDriver: LiveQueryDriver) { const h = driver.mount((q) => q .from({ left: left.collection }) - .join(leftJoin({ right: right.collection }, ({ left: a, right: b }: any) => - ops.eq(a.id, b.id))), + .join( + leftJoin( + { right: right.collection }, + ({ left: a, right: b }: any) => ops.eq(a.id, b.id), + ), + ), ) await h.flush() expect(h.current()).toMatchObject({ @@ -247,8 +255,12 @@ export function runSuite(rawDriver: LiveQueryDriver) { const h = driver.mount((q) => q .from({ opted: opted.collection }) - .join(leftJoin({ ordinary: ordinary.collection }, - ({ opted: a, ordinary: b }: any) => ops.eq(a.id, b.id))), + .join( + leftJoin( + { ordinary: ordinary.collection }, + ({ opted: a, ordinary: b }: any) => ops.eq(a.id, b.id), + ), + ), ) await h.apply(settle) await h.flush() @@ -346,10 +358,13 @@ export function runSuite(rawDriver: LiveQueryDriver) { q .from({ items: source.collection }) .where(({ items }: any) => ops.eq(items.team, team)) - const teamA = driver.mount(query(`a`)) - const teamB = driver.mount(query(`b`)) - const olderA = driver.mount((q) => - query(`a`)(q).where(({ items }: any) => ops.eq(items.age, 35)), + // The config opts into pooling; without it each query compiles. + const pool = { pool: pooledLiveQueries } + const teamA = driver.mountConfig(query(`a`), pool) + const teamB = driver.mountConfig(query(`b`), pool) + const olderA = driver.mountConfig( + (q) => query(`a`)(q).where(({ items }: any) => ops.eq(items.age, 35)), + pool, ) for (const h of [teamA, teamB, olderA]) await h.flush() // Pooling shares one subscription between the two team queries. @@ -635,8 +650,12 @@ export function runSuite(rawDriver: LiveQueryDriver) { const h = driver.mount((q) => q .from({ issues: issues.collection }) - .join(leftJoin({ persons: people.collection }, ({ issues: i, persons }: any) => - ops.eq(i.userId, persons.id))) + .join( + leftJoin( + { persons: people.collection }, + ({ issues: i, persons }: any) => ops.eq(i.userId, persons.id), + ), + ) .select(({ issues: i, persons }: any) => ({ id: i.id, title: i.title, @@ -699,8 +718,11 @@ export function runSuite(rawDriver: LiveQueryDriver) { })) return q .from({ persons: people.collection }) - .join(leftJoin({ ic: issueCounts }, ({ persons, ic }: any) => - ops.eq(persons.id, ic.userId))) + .join( + leftJoin({ ic: issueCounts }, ({ persons, ic }: any) => + ops.eq(persons.id, ic.userId), + ), + ) .select(({ persons, ic }: any) => ({ name: persons.name, issueCount: ic.issueCount, @@ -733,10 +755,12 @@ export function runSuite(rawDriver: LiveQueryDriver) { q.from({ persons: people.collection }).select(({ persons }: any) => ({ id: persons.id, name: persons.name, - issues: include(q - .from({ issues: issues.collection }) - .where(({ issues: i }: any) => ops.eq(i.userId, persons.id)) - .select(({ issues: i }: any) => ({ id: i.id, title: i.title }))), + issues: include( + q + .from({ issues: issues.collection }) + .where(({ issues: i }: any) => ops.eq(i.userId, persons.id)) + .select(({ issues: i }: any) => ({ id: i.id, title: i.title })), + ), })), ) await h.flush() diff --git a/packages/query-db-collection/tests/includes-work-counter-oracle.test.ts b/packages/query-db-collection/tests/includes-work-counter-oracle.test.ts index a36d8b9925..799aeaa260 100644 --- a/packages/query-db-collection/tests/includes-work-counter-oracle.test.ts +++ b/packages/query-db-collection/tests/includes-work-counter-oracle.test.ts @@ -1,10 +1,9 @@ -import { QueryClient } from '@tanstack/query-core' -import { - BasicIndex, +import { BasicIndex, createCollection, createLiveQueryCollection, eq, -} from '@tanstack/db' + include } from '@tanstack/db' +import { QueryClient } from '@tanstack/query-core' import { describe, expect, it } from 'vitest' import { evaluateReferenceExpression } from '../../db/tests/reference-expression-oracle' import { queryCollectionOptions } from '../src/query' @@ -363,27 +362,27 @@ async function checkNestedTreeShape( q.from({ root: sources.roots }).select(({ root }) => ({ id: root.id, value: root.value, - children: q + children: include(q .from({ branch: sources.branches }) .where(({ branch }) => eq(branch.parentId, root.id)) .select(({ branch }) => ({ id: branch.id, value: branch.value, - children: q + children: include(q .from({ twig: sources.twigs }) .where(({ twig }) => eq(twig.parentId, branch.id)) .select(({ twig }) => ({ id: twig.id, value: twig.value, - children: q + children: include(q .from({ leaf: sources.leaves }) .where(({ leaf }) => eq(leaf.parentId, twig.id)) .select(({ leaf }) => ({ id: leaf.id, value: leaf.value, - })), - })), - })), + }))), + }))), + }))), })), }) rootCollection = roots diff --git a/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts b/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts index 1eef409062..c91b62dd57 100644 --- a/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts +++ b/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts @@ -1,12 +1,11 @@ -import { QueryClient } from '@tanstack/query-core' -import { - BasicIndex, +import { BasicIndex, IR, createCollection, createLiveQueryCollection, eq, getLoadSubsetDemandKey, -} from '@tanstack/db' + leftJoin } from '@tanstack/db' +import { QueryClient } from '@tanstack/query-core' import { describe, expect, it, vi } from 'vitest' import { TraceAssertionError } from '../../db/tests/trace-runner.js' import { createDeferred } from '../../db/src/deferred.js' @@ -1301,9 +1300,8 @@ async function expectEveryFailedSourceToRecover(): Promise { const live = createLiveQueryCollection((query) => query .from({ left: left.collection }) - .join({ right: right.collection }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.id, rightRow.id), - ) + .join(leftJoin({ right: right.collection }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.id, rightRow.id))) .select(({ left: row }) => ({ id: row.id })), ) const preload = live.preload() diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index f4672f9d86..e10ce924df 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -1,14 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - QueryClient, - QueryObserver, - dehydrate, - focusManager, - hashKey, - onlineManager, -} from '@tanstack/query-core' -import { - BTreeIndex, +import { BTreeIndex, DbClient, IR, collectionOptions, @@ -18,9 +8,18 @@ import { eq, ilike, inArray, + include, or, - resetWarnings, -} from '@tanstack/db' + resetWarnings } from '@tanstack/db' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + QueryClient, + QueryObserver, + dehydrate, + focusManager, + hashKey, + onlineManager, +} from '@tanstack/query-core' import { mockSyncCollectionOptions, stripVirtualProps, @@ -1385,14 +1384,14 @@ describe(`QueryCollection`, () => { const collection = createLiveQueryCollection((q) => q.from({ lineItem: lineItems }).select(({ lineItem }) => ({ id: lineItem.id, - product: q + product: include(q .from({ product: products }) .where(({ product }) => eq(product.id, lineItem.productId)) .select(({ product }) => ({ id: product.id, categoryId: product.categoryId, name: product.name, - })), + }))), })), ) @@ -1465,13 +1464,13 @@ describe(`QueryCollection`, () => { const live = createLiveQueryCollection((q) => q.from({ lineItem: lineItems }).select(({ lineItem }) => ({ id: lineItem.id, - product: q + product: include(q .from({ product: products }) .where(({ product }) => eq(product.id, lineItem.productId)) .select(({ product }) => ({ id: product.id, name: product.name, - })), + }))), })), ) diff --git a/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts b/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts index f4d4ad4e90..4f5090247d 100644 --- a/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts +++ b/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts @@ -1,14 +1,13 @@ -import { QueryClient } from '@tanstack/query-core' -import { - BTreeIndex, +import { BTreeIndex, IR, createCollection, createLiveQueryCollection, eq, getLoadSubsetDemandKey, isUndefined, - not, -} from '@tanstack/db' + leftJoin, + not } from '@tanstack/db' +import { QueryClient } from '@tanstack/query-core' import fc from 'fast-check' import { describe, expect, it, vi } from 'vitest' import { @@ -639,9 +638,8 @@ async function startRun(testCase: Case): Promise { const window = createLiveQueryCollection((q) => { const ordered = q .from({ item: items }) - .leftJoin({ marker: markerCollection }, ({ item, marker }) => - eq(item.id, marker.itemId), - ) + .join(leftJoin({ marker: markerCollection }, ({ item, marker }) => + eq(item.id, marker.itemId))) .where(({ marker }) => not(isUndefined(marker.itemId))) .orderBy( ({ item }) => (scenario.orderKey === `rank` ? item.rank : item.label), diff --git a/packages/react-db/tests/conformance.test.tsx b/packages/react-db/tests/conformance.test.tsx index 19600d4867..cf935439e9 100644 --- a/packages/react-db/tests/conformance.test.tsx +++ b/packages/react-db/tests/conformance.test.tsx @@ -29,6 +29,7 @@ import { runSuite } from '../../db/tests/conformance/suite' import { makeLaggingSource } from '../../db/tests/conformance/lagging-source' import { expectResultSurface } from '../../db/tests/conformance/result-laws' import type { RenderHookResult } from '@testing-library/react' +import type { PooledLiveQueries } from '@tanstack/db' import type { ConformanceResult, ControllableHandle, @@ -137,7 +138,10 @@ function mountCollection(collection: any) { return makeHandle(hook) } -function mountConfig(build: QueryBuild, opts?: { gcTime?: number }) { +function mountConfig( + build: QueryBuild, + opts?: { gcTime?: number; pool?: PooledLiveQueries }, +) { const hook = renderHook(() => useLiveQuery({ query: build as any, ...opts })) return makeHandle(hook) } diff --git a/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx b/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx index 5042bef2d4..de6d6ca312 100644 --- a/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx +++ b/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx @@ -1,3 +1,13 @@ +import { DbClient, Query, collectionOptions, + createCollection, + eq, + getStableQueryBuilderHash, + innerJoin, + isUndefined, + joinOf, + leftJoin, + prepareLiveQueryValue, + toArray } from '@tanstack/db' /** * A Query assembled with a Collection descriptor is a reusable query plan. * Its source Collection belongs to the DbClient that later consumes it, so @@ -83,17 +93,6 @@ * histories do not cover arbitrary concurrent render schedules. */ import { act, render, renderHook, waitFor } from '@testing-library/react' -import { - DbClient, - Query, - collectionOptions, - createCollection, - eq, - getStableQueryBuilderHash, - isUndefined, - prepareLiveQueryValue, - toArray, -} from '@tanstack/db' import { describe, expect, it } from 'vitest' import { Suspense } from 'react' import { DbProvider } from '../src/DbProvider' @@ -187,7 +186,7 @@ describe(`standalone descriptor query binding`, () => { firstRenderDeferrals.clear() const continued = prepared - .join({ late: lateDescriptor }, ({ base, late }) => eq(base.id, late.id)) + .join(leftJoin({ late: lateDescriptor }, ({ base, late }) => eq(base.id, late.id))) .select(({ base, late }) => ({ id: base.id, value: late.value })) expect(lateStarts).toBe(0) @@ -235,9 +234,8 @@ describe(`standalone descriptor query binding`, () => { client, deferrals, ) as typeof original - prepared.join({ late: lateDescriptor }, ({ base, late }) => - eq(base.id, late.id), - ) + prepared.join(leftJoin({ late: lateDescriptor }, ({ base, late }) => + eq(base.id, late.id))) throw never } @@ -857,9 +855,8 @@ describe(`standalone descriptor query binding`, () => { const joined = () => new Query() .from({ parent: parentDescriptor }) - .innerJoin({ child: childDescriptor }, ({ parent, child }) => - eq(parent.id, child.parentId), - ) + .join(innerJoin({ child: childDescriptor }, ({ parent, child }) => + eq(parent.id, child.parentId))) .select(({ parent, child }) => ({ id: parent.id, value: parent.value, @@ -1343,15 +1340,12 @@ describe(`standalone descriptor query binding`, () => { : anchors const child = new Query() .from({ [alias]: mains }) - .join( - { anchor: joinedSource }, + .join(joinOf(kind, { anchor: joinedSource }, (context: Record) => eq( (context[alias] as Main).anchorId, (context.anchor as Anchor).id, - ), - kind, - ) + ))) .where((context: Record) => correlation === `joined` ? eq((context.anchor as Anchor).parentId, parent.id) diff --git a/packages/react-db/tests/source-id-reuse-oracle.test.tsx b/packages/react-db/tests/source-id-reuse-oracle.test.tsx index aacbe853bc..77b2b3685f 100644 --- a/packages/react-db/tests/source-id-reuse-oracle.test.tsx +++ b/packages/react-db/tests/source-id-reuse-oracle.test.tsx @@ -1,3 +1,9 @@ +import { DbClient, + collectionOptions, + createCollection, + eq, + gt, + leftJoin } from '@tanstack/db' /** * Source ID reuse contract for React's derived live-query identity. * @@ -29,13 +35,6 @@ * arrays, or explicit-key semantics; explicit queryKey callers own their key. */ import { act, render, renderHook, waitFor } from '@testing-library/react' -import { - DbClient, - collectionOptions, - createCollection, - eq, - gt, -} from '@tanstack/db' import { describe, expect, it, vi } from 'vitest' import { Component, Suspense } from 'react' import { DbProvider } from '../src/DbProvider' @@ -150,15 +149,12 @@ describe(`React source ID reuse`, () => { include ? q .from({ failingA: failing[0]! }) - .join({ failingB: failing[1]! }, ({ failingA, failingB }) => - eq(failingA.id, failingB.id), - ) - .join({ healthy }, ({ failingA, healthy }) => - eq(failingA.id, healthy.id), - ) - .join({ settings: current }, ({ failingA, settings }) => - eq(failingA.id, settings.id), - ) + .join(leftJoin({ failingB: failing[1]! }, ({ failingA, failingB }) => + eq(failingA.id, failingB.id))) + .join(leftJoin({ healthy }, ({ failingA, healthy }) => + eq(failingA.id, healthy.id))) + .join(leftJoin({ settings: current }, ({ failingA, settings }) => + eq(failingA.id, settings.id))) : q.from({ settings: current }), }) return

Query
@@ -226,9 +222,8 @@ describe(`React source ID reuse`, () => { includeShared ? q .from({ settings: current }) - .join({ shared: descriptor }, ({ settings, shared }) => - eq(settings.id, shared.id), - ) + .join(leftJoin({ shared: descriptor }, ({ settings, shared }) => + eq(settings.id, shared.id))) : q.from({ settings: current }), }) return
Query
@@ -339,9 +334,8 @@ describe(`React source ID reuse`, () => { query: (q) => q .from({ first: leftSource }) - .join({ second: rightSource }, ({ first, second }) => - eq(first.id, second.id), - ), + .join(leftJoin({ second: rightSource }, ({ first, second }) => + eq(first.id, second.id))), }), ), ).toThrow(/source Collection .*same ID/i) @@ -726,9 +720,8 @@ describe(`React source ID reuse: useLiveInfiniteQuery`, () => { includeShared ? q .from({ settings: current }) - .join({ shared: descriptor }, ({ settings, shared }) => - eq(settings.id, shared.id), - ) + .join(leftJoin({ shared: descriptor }, ({ settings, shared }) => + eq(settings.id, shared.id))) .orderBy(({ settings }) => settings.rank) : q .from({ settings: current }) diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index 6d09e4152f..c4cde7d8e4 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -1,8 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' -import { act, renderHook, waitFor } from '@testing-library/react' -import { - DbClient, - Query, +import { DbClient, Query , coalesce, collectionOptions, count, @@ -11,11 +7,14 @@ import { createOptimisticAction, eq, gt, + include, + leftJoin, localOnlyCollectionOptions, lte, sum, - toArray, -} from '@tanstack/db' + toArray } from '@tanstack/db' +import { describe, expect, it, vi } from 'vitest' +import { act, renderHook, waitFor } from '@testing-library/react' import { useEffect } from 'react' import { useLiveQuery, useLiveQueryForSuspense } from '../src/useLiveQuery' import { getLiveQueryResultInfo } from '../src/live-query-internals' @@ -447,9 +446,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -822,9 +820,8 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1460,9 +1457,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1886,9 +1882,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -2640,9 +2635,8 @@ describe(`Query Collections`, () => { return q .from({ persons: personCollection }) - .leftJoin({ ic: issueCountSubquery }, ({ persons, ic }) => - eq(persons.id, ic.userId), - ) + .join(leftJoin({ ic: issueCountSubquery }, ({ persons, ic }) => + eq(persons.id, ic.userId))) .select(({ persons, ic }) => ({ name: persons.name, issueCount: ic.issueCount, @@ -2761,13 +2755,13 @@ describe(`Query Collections`, () => { q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q + issues: include(q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) .select(({ i }) => ({ id: i.id, title: i.title, - })), + }))), })), ) }) diff --git a/packages/solid-db/tests/conformance.test.tsx b/packages/solid-db/tests/conformance.test.tsx index 232543cb2d..1780879cb2 100644 --- a/packages/solid-db/tests/conformance.test.tsx +++ b/packages/solid-db/tests/conformance.test.tsx @@ -30,6 +30,7 @@ import { runSuite } from '../../db/tests/conformance/suite' import { makeLaggingSource } from '../../db/tests/conformance/lagging-source' import { expectResultSurface } from '../../db/tests/conformance/result-laws' import { withScopeSetup } from '../../db/tests/conformance/scope-setup' +import type { PooledLiveQueries } from '@tanstack/db' import type { ConformanceResult, ControllableHandle, @@ -190,7 +191,10 @@ function mountCollection(collection: any) { return makeHandle(getResult, dispose) } -function mountConfig(build: QueryBuild, opts?: { gcTime?: number }) { +function mountConfig( + build: QueryBuild, + opts?: { gcTime?: number; pool?: PooledLiveQueries }, +) { // Solid accepts the config-object form via an accessor. const { getResult, dispose } = inRoot(() => useLiveQuery(() => ({ query: build, ...opts })), diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index 85f90c60dd..37792338f6 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -1,16 +1,15 @@ -import { describe, expect, it, vi } from 'vitest' -import { render, renderHook, waitFor } from '@solidjs/testing-library' -import { - Query, - count, +import { Query, count , createCollection, createLiveQueryCollection, createOptimisticAction, eq, gt, + include, + leftJoin, toArray, - waitForPersistedInitialRender, -} from '@tanstack/db' + waitForPersistedInitialRender } from '@tanstack/db' +import { describe, expect, it, vi } from 'vitest' +import { render, renderHook, waitFor } from '@solidjs/testing-library' import { For, Suspense, @@ -382,9 +381,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -866,9 +864,8 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1440,9 +1437,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1838,9 +1834,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -3468,14 +3463,14 @@ describe(`includes subqueries`, () => { useLiveQuery((q) => q.from({ project: projects }).select(({ project }) => ({ id: project.id, - issues: q + issues: include(q .from({ issue: issues }) .where(({ issue }) => eq(issue.projectId, project.id)) .select(({ issue }) => ({ id: issue.id, projectId: issue.projectId, title: issue.title, - })), + }))), })), ), ) diff --git a/packages/svelte-db/tests/conformance.svelte.test.ts b/packages/svelte-db/tests/conformance.svelte.test.ts index 0868687354..576a9965a6 100644 --- a/packages/svelte-db/tests/conformance.svelte.test.ts +++ b/packages/svelte-db/tests/conformance.svelte.test.ts @@ -28,6 +28,7 @@ import { useLiveQuery } from '../src/useLiveQuery.svelte.js' import { runSuite } from '../../db/tests/conformance/suite' import { makeLaggingSource } from '../../db/tests/conformance/lagging-source' import { expectResultSurface } from '../../db/tests/conformance/result-laws' +import type { PooledLiveQueries } from '@tanstack/db' import type { ConformanceResult, ControllableHandle, @@ -178,7 +179,10 @@ function mountCollection(collection: any) { return makeHandle(() => query, dispose) } -function mountConfig(build: QueryBuild, opts?: { gcTime?: number }) { +function mountConfig( + build: QueryBuild, + opts?: { gcTime?: number; pool?: PooledLiveQueries }, +) { let query: any const dispose = $effect.root(() => { query = useLiveQuery({ query: build, ...opts } as any) diff --git a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts index 1dfd8694c1..fc6ef847f9 100644 --- a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts +++ b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts @@ -1,6 +1,4 @@ -import { afterEach, describe, expect, it } from 'vitest' -import { - BaseQueryBuilder, +import { BaseQueryBuilder, DbClient, count, createCollection, @@ -9,7 +7,8 @@ import { getLiveQueryHash, getStableValueHash, gt, -} from '@tanstack/db' + leftJoin } from '@tanstack/db' +import { afterEach, describe, expect, it } from 'vitest' import { flushSync } from 'svelte' import { useLiveQuery } from '../src/useLiveQuery.svelte.js' import { mockSyncCollectionOptions } from '../../db/tests/utils' @@ -489,9 +488,8 @@ describe(`Query Collections`, () => { const query = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -775,9 +773,8 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1290,9 +1287,8 @@ describe(`Query Collections`, () => { const query = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1842,9 +1838,8 @@ describe(`Query Collections`, () => { const query = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, diff --git a/packages/vue-db/tests/conformance.test.ts b/packages/vue-db/tests/conformance.test.ts index 807a847e66..c35e4cb90b 100644 --- a/packages/vue-db/tests/conformance.test.ts +++ b/packages/vue-db/tests/conformance.test.ts @@ -29,6 +29,7 @@ import { runSuite } from '../../db/tests/conformance/suite' import { makeLaggingSource } from '../../db/tests/conformance/lagging-source' import { expectResultSurface } from '../../db/tests/conformance/result-laws' import { withScopeSetup } from '../../db/tests/conformance/scope-setup' +import type { PooledLiveQueries } from '@tanstack/db' import type { ConformanceResult, ControllableHandle, @@ -188,7 +189,10 @@ function mountCollection(collection: any) { return makeHandle(result, scope) } -function mountConfig(build: QueryBuild, opts?: { gcTime?: number }) { +function mountConfig( + build: QueryBuild, + opts?: { gcTime?: number; pool?: PooledLiveQueries }, +) { const { result, scope } = runInScope(() => useLiveQuery({ query: build, ...opts } as any), ) diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index 58e43da6fd..d3e2755e0b 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -1,12 +1,11 @@ -import { describe, expect, it } from 'vitest' -import { - count, +import { count, createCollection, createLiveQueryCollection, createOptimisticAction, eq, gt, -} from '@tanstack/db' + leftJoin } from '@tanstack/db' +import { describe, expect, it } from 'vitest' import { nextTick, ref, watchEffect } from 'vue' import { useLiveQuery } from '../src/useLiveQuery' import { mockSyncCollectionOptions } from '../../db/tests/utils' @@ -316,9 +315,8 @@ describe(`Query Collections`, () => { const { state } = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -605,9 +603,8 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1177,9 +1174,8 @@ describe(`Query Collections`, () => { const { isReady } = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1704,9 +1700,8 @@ describe(`Query Collections`, () => { const { isLoading, state, data } = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ) + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id))) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, From 8b721ec376c500305d9f087c0e9b36fdd810c39f Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 16:32:41 -0600 Subject: [PATCH 07/32] docs: document opt-in operators and pooling The live-queries guide, JSDoc examples, and generated reference use join operators, include(), groupKeys(), and pooledLiveQueries. The join size scenario uses leftJoin; size:compare reports n/a for a scenario that cannot build against an older base. Co-authored-by: Isaac --- docs/errors.md | 40 +-- .../react/reference/functions/useLiveQuery.md | 58 ++-- .../solid/reference/functions/useLiveQuery.md | 12 +- .../reference/functions/useLiveQuery.md | 12 +- .../vue/reference/functions/useLiveQuery.md | 12 +- docs/guides/live-queries.md | 191 +++++++----- docs/overview.md | 14 +- .../namespaces/IR/classes/Aggregate.md | 134 ++++++++- .../namespaces/IR/classes/CollectionRef.md | 16 +- .../IR/classes/ConditionalSelect.md | 12 +- .../namespaces/IR/classes/DescriptorRef.md | 16 +- .../@tanstack/namespaces/IR/classes/Func.md | 12 +- .../namespaces/IR/classes/IncludesSubquery.md | 89 +++++- .../namespaces/IR/classes/PropRef.md | 14 +- .../namespaces/IR/classes/QueryRef.md | 14 +- .../namespaces/IR/classes/UnionAll.md | 12 +- .../namespaces/IR/classes/UnionFrom.md | 12 +- .../@tanstack/namespaces/IR/classes/Value.md | 10 +- .../IR/functions/collectCollectionSources.md | 2 +- .../IR/functions/collectPropRefs.md | 2 +- .../IR/functions/collectSourceRefs.md | 2 +- .../IR/functions/createResidualWhere.md | 2 +- .../namespaces/IR/functions/followRef.md | 2 +- .../namespaces/IR/functions/getFromSources.md | 2 +- .../IR/functions/getHavingExpression.md | 2 +- .../IR/functions/getPropRefPropertyPath.md | 2 +- .../IR/functions/getPropRefSourceAlias.md | 2 +- .../IR/functions/getWhereExpression.md | 2 +- .../functions/isBasicOrAggregateExpression.md | 2 +- .../IR/functions/isExpressionLike.md | 2 +- .../IR/functions/isResidualWhere.md | 2 +- .../IR/functions/requireCollectionSource.md | 4 +- .../namespaces/IR/interfaces/JoinClause.md | 187 +++++++++++- .../namespaces/IR/interfaces/QueryIR.md | 137 ++++++++- .../IR/type-aliases/BasicExpression.md | 2 +- .../IR/type-aliases/CollectionSourceRef.md | 2 +- .../type-aliases/ConditionalSelectBranch.md | 6 +- .../namespaces/IR/type-aliases/From.md | 2 +- .../namespaces/IR/type-aliases/GroupBy.md | 2 +- .../namespaces/IR/type-aliases/Having.md | 2 +- .../type-aliases/IncludesMaterialization.md | 2 +- .../namespaces/IR/type-aliases/Join.md | 2 +- .../namespaces/IR/type-aliases/Limit.md | 2 +- .../namespaces/IR/type-aliases/Offset.md | 2 +- .../namespaces/IR/type-aliases/OrderBy.md | 2 +- .../IR/type-aliases/OrderByClause.md | 6 +- .../IR/type-aliases/OrderByDirection.md | 2 +- .../namespaces/IR/type-aliases/Select.md | 2 +- .../IR/type-aliases/SelectValueExpression.md | 2 +- .../namespaces/IR/type-aliases/Where.md | 2 +- .../IR/variables/INCLUDES_SCALAR_FIELD.md | 2 +- .../AggregateFunctionNotInSelectError.md | 4 +- docs/reference/classes/BaseQueryBuilder.md | 272 +++--------------- .../CannotCombineEmptyExpressionListError.md | 4 +- .../classes/CollectionInputNotFoundError.md | 4 +- .../classes/CollectionPreloadAbortedError.md | 4 +- .../classes/DistinctRequiresSelectError.md | 4 +- .../classes/DuplicateAliasInSubqueryError.md | 4 +- .../classes/EmptyReferencePathError.md | 4 +- .../classes/FnSelectWithGroupByError.md | 4 +- docs/reference/classes/GroupByError.md | 4 +- .../classes/GroupingOperatorRequiredError.md | 212 ++++++++++++++ .../classes/HavingRequiresGroupByError.md | 4 +- .../classes/IncludeOperatorRequiredError.md | 218 ++++++++++++++ .../reference/classes/InvalidJoinCondition.md | 4 +- .../InvalidJoinConditionLeftSourceError.md | 4 +- .../InvalidJoinConditionRightSourceError.md | 4 +- .../InvalidJoinConditionSameSourceError.md | 4 +- ...InvalidJoinConditionSourceMismatchError.md | 4 +- .../classes/InvalidSourceTypeError.md | 4 +- .../classes/InvalidStorageDataFormatError.md | 4 +- .../InvalidStorageObjectFormatError.md | 4 +- .../classes/InvalidWhereExpressionError.md | 4 +- .../classes/JoinCollectionNotFoundError.md | 4 +- .../JoinConditionMustBeEqualityError.md | 4 +- docs/reference/classes/JoinError.md | 4 +- .../classes/JoinOperatorRequiredError.md | 218 ++++++++++++++ .../classes/LimitOffsetRequireOrderByError.md | 4 +- .../LoadSubsetOperationAbortedError.md | 4 +- .../classes/LocalStorageCollectionError.md | 4 +- .../classes/MissingAliasInputsError.md | 4 +- ...NonAggregateExpressionNotInGroupByError.md | 4 +- docs/reference/classes/QueryBuilderError.md | 2 + .../classes/QueryCompilationError.md | 5 +- .../classes/QueryMustHaveFromClauseError.md | 4 +- docs/reference/classes/QueryOptimizerError.md | 4 +- docs/reference/classes/SerializationError.md | 4 +- .../classes/SetWindowFindOneLimitError.md | 4 +- .../classes/SetWindowReentrancyError.md | 4 +- .../classes/SetWindowRequiresOrderByError.md | 4 +- docs/reference/classes/StorageError.md | 4 +- .../classes/StorageKeyRequiredError.md | 4 +- docs/reference/classes/SyncCleanupError.md | 4 +- .../classes/SyncTransactionAbortedError.md | 4 +- .../classes/UnhashableQueryIRError.md | 8 +- .../classes/UnknownExpressionTypeError.md | 4 +- .../reference/classes/UnknownFunctionError.md | 4 +- .../UnknownHavingExpressionTypeError.md | 4 +- .../reference/classes/UnsafeAliasPathError.md | 4 +- .../UnsupportedAggregateFunctionError.md | 4 +- .../classes/UnsupportedFnSelectResultError.md | 4 +- .../classes/UnsupportedFromTypeError.md | 4 +- .../classes/UnsupportedJoinSourceTypeError.md | 4 +- .../classes/UnsupportedJoinTypeError.md | 4 +- .../UnsupportedRootScalarSelectError.md | 4 +- docs/reference/functions/compileExpression.md | 4 +- docs/reference/functions/compileQuery.md | 2 +- .../functions/createLiveQueryObserver.md | 2 +- .../functions/getPersistedReadinessSource.md | 2 +- .../functions/getPreparedLiveQueryIdentity.md | 16 +- .../functions/getPreparedLiveQuerySources.md | 2 +- docs/reference/functions/groupKeys.md | 26 ++ docs/reference/functions/include.md | 40 +++ docs/reference/functions/joinOf.md | 47 +++ docs/reference/functions/materialize.md | 2 +- .../functions/prepareLiveQueryValue.md | 2 +- .../functions/resolveLiveQueryValue.md | 10 +- .../resumeDeferredLiveQueryCollections.md | 2 +- docs/reference/functions/toArray.md | 2 +- .../waitForPersistedInitialRender.md | 40 +++ docs/reference/index.md | 17 ++ docs/reference/interfaces/Context.md | 24 +- .../CreateLiveQueryObserverOptions.md | 10 +- docs/reference/interfaces/GroupKeys.md | 132 +++++++++ docs/reference/interfaces/JoinOperator.md | 254 ++++++++++++++++ .../interfaces/LiveQueryCollectionConfig.md | 87 +++++- .../interfaces/PersistedInitialRenderInput.md | 118 ++++++++ .../interfaces/PersistedReadinessSource.md | 54 +++- .../ApplyJoinOptionalityToMergedSchema.md | 2 +- .../type-aliases/ContextFromSource.md | 10 +- .../type-aliases/ContextFromUnionBranches.md | 18 +- .../type-aliases/ContextFromUnionSource.md | 2 +- docs/reference/type-aliases/ContextSchema.md | 2 +- .../DeferredLiveQueryCollections.md | 2 +- docs/reference/type-aliases/ExtractContext.md | 2 +- .../type-aliases/FunctionalHavingRow.md | 2 +- docs/reference/type-aliases/GetResult.md | 2 +- .../reference/type-aliases/GroupByCallback.md | 2 +- .../type-aliases/InferCollectionType.md | 2 +- .../reference/type-aliases/InferResultType.md | 2 +- .../type-aliases/InitialQueryBuilder.md | 2 +- docs/reference/type-aliases/JoinOnCallback.md | 2 +- docs/reference/type-aliases/JoinType.md | 10 + .../type-aliases/LiveQueryCollectionUtils.md | 2 +- docs/reference/type-aliases/LiveQueryKey.md | 2 +- .../type-aliases/LiveQueryOptions.md | 2 +- .../MergeContextForJoinCallback.md | 6 +- .../type-aliases/MergeContextWithJoinType.md | 2 +- .../reference/type-aliases/OrderByCallback.md | 2 +- .../PersistedReadinessSnapshot.md | 2 +- .../type-aliases/PooledLiveQueries.md | 10 + docs/reference/type-aliases/Prettify.md | 2 +- docs/reference/type-aliases/QueryBuilder.md | 2 +- docs/reference/type-aliases/QueryResult.md | 2 +- docs/reference/type-aliases/Ref.md | 2 +- docs/reference/type-aliases/RefsForContext.md | 2 +- .../type-aliases/ResultTypeFromSelect.md | 2 +- .../type-aliases/SchemaFromSource.md | 2 +- docs/reference/type-aliases/SelectObject.md | 2 +- docs/reference/type-aliases/SingleSource.md | 2 +- docs/reference/type-aliases/Source.md | 4 +- .../type-aliases/SourceClauseContext.md | 2 +- docs/reference/type-aliases/WhereCallback.md | 2 +- docs/reference/type-aliases/WithResult.md | 2 +- .../variables/PERSISTED_READINESS.md | 2 +- docs/reference/variables/Query.md | 2 +- docs/reference/variables/fullJoin.md | 34 +++ docs/reference/variables/innerJoin.md | 34 +++ docs/reference/variables/leftJoin.md | 34 +++ docs/reference/variables/pooledLiveQueries.md | 64 +++++ docs/reference/variables/rightJoin.md | 34 +++ packages/db-collection-e2e/README.md | 6 +- packages/db/src/query/builder/index.ts | 15 +- packages/db/src/query/builder/types.ts | 6 +- packages/db/src/query/optimizer.ts | 10 +- packages/react-db/src/useLiveQuery.ts | 4 +- packages/solid-db/src/useLiveQuery.ts | 4 +- packages/svelte-db/src/useLiveQuery.svelte.ts | 4 +- packages/vue-db/src/useLiveQuery.ts | 4 +- scripts/size-compare.mjs | 18 +- scripts/size-scenarios/app-join.js | 6 +- 181 files changed, 2733 insertions(+), 760 deletions(-) create mode 100644 docs/reference/classes/GroupingOperatorRequiredError.md create mode 100644 docs/reference/classes/IncludeOperatorRequiredError.md create mode 100644 docs/reference/classes/JoinOperatorRequiredError.md create mode 100644 docs/reference/functions/groupKeys.md create mode 100644 docs/reference/functions/include.md create mode 100644 docs/reference/functions/joinOf.md create mode 100644 docs/reference/functions/waitForPersistedInitialRender.md create mode 100644 docs/reference/interfaces/GroupKeys.md create mode 100644 docs/reference/interfaces/JoinOperator.md create mode 100644 docs/reference/interfaces/PersistedInitialRenderInput.md create mode 100644 docs/reference/type-aliases/JoinType.md create mode 100644 docs/reference/type-aliases/PooledLiveQueries.md create mode 100644 docs/reference/variables/fullJoin.md create mode 100644 docs/reference/variables/innerJoin.md create mode 100644 docs/reference/variables/leftJoin.md create mode 100644 docs/reference/variables/pooledLiveQueries.md create mode 100644 docs/reference/variables/rightJoin.md diff --git a/docs/errors.md b/docs/errors.md index 2a647cc9ad..ec6494190f 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -990,32 +990,42 @@ Development builds show the full message. For example: setWindow() cannot change the limit of a findOne() query. ``` - + -## Error 247: `IncludeOperatorRequiredError` +## Error 245: `JoinOperatorRequiredError` Development builds show the full message. For example: ```text -A subquery in select() needs include(), toArray(), or materialize(). Wrap the issues field: issues: include(q.from({ i: issues }).where(...)). Import include from @tanstack/db. +.join() takes a join operator, such as .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Import leftJoin, innerJoin, rightJoin, fullJoin, or joinOf from @tanstack/db. Got: object ``` -## Other errors and log messages + -These are plain `Error`, `TypeError`, `RangeError`, or -`AggregateError` values, and `console` messages about runtime failures. -Each heading names the source file under `packages/db/src` that writes it. +## Error 246: `GroupingOperatorRequiredError` - +Development builds show the full message. For example: -## Error 245: `JoinOperatorRequiredError` +```text +groupBy() without an aggregate needs groupKeys(), such as .groupBy(groupKeys(({ todo }) => todo.status)). Import groupKeys from @tanstack/db. A query with an aggregate such as count() needs nothing extra. +``` + + + +## Error 247: `IncludeOperatorRequiredError` Development builds show the full message. For example: ```text -.join() takes a join operator, such as .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Import leftJoin, innerJoin, rightJoin, fullJoin, or joinOf from @tanstack/db. Got: object +A subquery in select() needs include(), toArray(), or materialize(). Wrap the issues field: issues: include(q.from({ i: issues }).where(...)). Import include from @tanstack/db. ``` +## Other errors and log messages + +These are plain `Error`, `TypeError`, `RangeError`, or +`AggregateError` values, and `console` messages about runtime failures. +Each heading names the source file under `packages/db/src` that writes it. + ## Error 88: `client.ts` @@ -2489,13 +2499,3 @@ Development builds show this message, with `${...}` replaced by values: ```text A windowed or findOne result row has no include route ``` - - - -## Error 246: `GroupingOperatorRequiredError` - -Development builds show the full message. For example: - -```text -groupBy() without an aggregate needs groupKeys(), such as .groupBy(groupKeys(({ todo }) => todo.status)). Import groupKeys from @tanstack/db. A query with an aggregate such as count() needs nothing extra. -``` diff --git a/docs/framework/react/reference/functions/useLiveQuery.md b/docs/framework/react/reference/functions/useLiveQuery.md index 7e8833b9be..9d3e8fbe5c 100644 --- a/docs/framework/react/reference/functions/useLiveQuery.md +++ b/docs/framework/react/reference/functions/useLiveQuery.md @@ -9,7 +9,7 @@ title: useLiveQuery function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:387](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L387) +Defined in: [useLiveQuery.ts:396](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L396) Create a live query using a query function. @@ -163,9 +163,9 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -196,7 +196,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:407](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L407) +Defined in: [useLiveQuery.ts:416](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L416) Create a live query using a query function. @@ -354,9 +354,9 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -387,7 +387,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:429](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L429) +Defined in: [useLiveQuery.ts:438](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L438) Create a live query using a query function. @@ -548,9 +548,9 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -581,7 +581,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:451](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L451) +Defined in: [useLiveQuery.ts:460](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L460) Create a live query using a query function. @@ -747,9 +747,9 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -780,7 +780,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:477](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L477) +Defined in: [useLiveQuery.ts:486](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L486) Create a live query using a query function. @@ -957,9 +957,9 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -990,7 +990,7 @@ return ( function useLiveQuery(config): object; ``` -Defined in: [useLiveQuery.ts:548](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L548) +Defined in: [useLiveQuery.ts:557](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L557) Create a live query using configuration object @@ -1134,7 +1134,7 @@ return
{data.length} items loaded
function useLiveQuery(config): object; ``` -Defined in: [useLiveQuery.ts:567](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L567) +Defined in: [useLiveQuery.ts:576](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L576) Create a live query using a query function. @@ -1284,9 +1284,9 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1317,7 +1317,7 @@ return ( function useLiveQuery(config, deps?): object; ``` -Defined in: [useLiveQuery.ts:586](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L586) +Defined in: [useLiveQuery.ts:595](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L595) Create a live query using a query function. @@ -1469,9 +1469,9 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1502,7 +1502,7 @@ return ( function useLiveQuery(config, deps): object; ``` -Defined in: [useLiveQuery.ts:606](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L606) +Defined in: [useLiveQuery.ts:615](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L615) Create a live query using a query function. @@ -1658,9 +1658,9 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1691,7 +1691,7 @@ return ( function useLiveQuery(liveQueryCollection): object; ``` -Defined in: [useLiveQuery.ts:655](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L655) +Defined in: [useLiveQuery.ts:664](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L664) Subscribe to an existing live query collection @@ -1837,7 +1837,7 @@ return
{data.map(item => )}
function useLiveQuery(liveQueryCollection): object; ``` -Defined in: [useLiveQuery.ts:678](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L678) +Defined in: [useLiveQuery.ts:687](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L687) Create a live query using a query function. @@ -1990,9 +1990,9 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, diff --git a/docs/framework/solid/reference/functions/useLiveQuery.md b/docs/framework/solid/reference/functions/useLiveQuery.md index 065ee1bc2c..9237b7468d 100644 --- a/docs/framework/solid/reference/functions/useLiveQuery.md +++ b/docs/framework/solid/reference/functions/useLiveQuery.md @@ -54,9 +54,9 @@ const todosQuery = useLiveQuery( // Join pattern const personIssues = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -154,9 +154,9 @@ const todosQuery = useLiveQuery( // Join pattern const personIssues = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -401,9 +401,9 @@ const todosQuery = useLiveQuery( // Join pattern const personIssues = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, diff --git a/docs/framework/svelte/reference/functions/useLiveQuery.md b/docs/framework/svelte/reference/functions/useLiveQuery.md index cd2ade1052..f35f499b1f 100644 --- a/docs/framework/svelte/reference/functions/useLiveQuery.md +++ b/docs/framework/svelte/reference/functions/useLiveQuery.md @@ -98,9 +98,9 @@ const { data, isLoading, isError } = $derived(query) // Join pattern const issuesQuery = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -224,9 +224,9 @@ const { data, isLoading, isError } = $derived(query) // Join pattern const issuesQuery = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -506,9 +506,9 @@ const { data, isLoading, isError } = $derived(query) // Join pattern const issuesQuery = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, diff --git a/docs/framework/vue/reference/functions/useLiveQuery.md b/docs/framework/vue/reference/functions/useLiveQuery.md index 86d6402dac..9f88603305 100644 --- a/docs/framework/vue/reference/functions/useLiveQuery.md +++ b/docs/framework/vue/reference/functions/useLiveQuery.md @@ -64,9 +64,9 @@ const { data, state } = useLiveQuery( // Join pattern const { data } = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -150,9 +150,9 @@ const { data, state } = useLiveQuery( // Join pattern const { data } = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -383,9 +383,9 @@ const { data, state } = useLiveQuery( // Join pattern const { data } = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => + .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) - ) + )) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index 8b1ea145c8..5ff9cc16af 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -892,7 +892,7 @@ For a complete list of available functions, see the [Expression Functions Refere ## Joins -Use `join` to combine data from multiple collections. Joins default to `left` join type and support equality conditions combined with `and()`. +Use `join` with a join operator to combine data from multiple collections. Import `leftJoin`, `innerJoin`, `rightJoin`, or `fullJoin` from `@tanstack/db` and pass it to `join`. Join conditions are equalities, optionally combined with `and()`. The operator brings the join code with it, so an app that never joins does not bundle it. Joins in TanStack DB are a way to combine data from multiple collections, and are conceptually very similar to SQL joins. When two collections are joined, the result is a new collection that contains the combined data as single rows. The new collection is a live query collection, and will automatically update when the underlying data changes. @@ -906,17 +906,26 @@ The result type of a join will take into account the join type, with the optiona ### Method Signature ```ts -join( +join(operator: JoinOperator): Query + +leftJoin( { [alias]: Collection | Query }, condition: (row: TRow) => Expression, // `eq(...)` or `and(eq(...), ...)` - joinType?: 'left' | 'right' | 'inner' | 'full' -): Query +): JoinOperator // also innerJoin, rightJoin, fullJoin + +joinOf( + joinType: 'left' | 'right' | 'inner' | 'full', + { [alias]: Collection | Query }, + condition: (row: TRow) => Expression, +): JoinOperator ``` **Parameters:** - `aliases` - An object where keys are alias names and values are collections or subqueries to join - `condition` - A callback function that receives the combined row object and returns an equality or a conjunction of equalities -- `joinType` - Optional join type: `'left'` (default), `'right'`, `'inner'`, or `'full'` +- `joinType` - For `joinOf`, a join type chosen at run time + +Pass the operator inline, as in `.join(leftJoin(...))`, so its condition callback is typed from the query. ### Basic Joins @@ -926,9 +935,9 @@ Join users with their posts: const userPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join({ post: postsCollection }, ({ user, post }) => + .join(leftJoin({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId) - ) + )) ) /* @@ -953,9 +962,9 @@ Use `and()` when rows must match on more than one field: ```ts const inventory = createLiveQueryCollection((q) => q.from({ product: productsCollection }) - .innerJoin({ stock: inventoryCollection }, ({ product, stock }) => + .join(innerJoin({ stock: inventoryCollection }, ({ product, stock }) => and(eq(product.sku, stock.sku), eq(product.region, stock.region)) - ) + )) ) ``` @@ -983,21 +992,18 @@ no keyed demand when lazy loading applies. ### Join Types -Specify the join type as the third parameter: +Choose the join type with the operator you import: ```ts const activeUserPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join( - { post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId), - 'inner', // `inner`, `left`, `right` or `full` - ) + .join(innerJoin({ post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId))) ) ``` -Or using the aliases `leftJoin`, `rightJoin`, `innerJoin` and `fullJoin` methods: +Each join type has its own operator: ### Left Join ```ts @@ -1005,10 +1011,8 @@ Or using the aliases `leftJoin`, `rightJoin`, `innerJoin` and `fullJoin` methods const allUsers = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .leftJoin( - { post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId), - ) + .join(leftJoin({ post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId))) ) /* @@ -1027,10 +1031,8 @@ Result type: const allPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .rightJoin( - { post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId), - ) + .join(rightJoin({ post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId))) ) /* @@ -1049,10 +1051,8 @@ Result type: const activeUserPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .innerJoin( - { post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId), - ) + .join(innerJoin({ post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId))) ) /* @@ -1071,10 +1071,8 @@ Result type: const allUsersAndPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .fullJoin( - { post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId), - ) + .join(fullJoin({ post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId))) ) /* @@ -1094,12 +1092,12 @@ Chain multiple joins in a single query: const userPostComments = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join({ post: postsCollection }, ({ user, post }) => + .join(leftJoin({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId) - ) - .join({ comment: commentsCollection }, ({ post, comment }) => + )) + .join(leftJoin({ comment: commentsCollection }, ({ post, comment }) => eq(post.id, comment.postId) - ) + )) .select(({ user, post, comment }) => ({ userName: user.name, postTitle: post.title, @@ -1131,9 +1129,9 @@ const activeUserPosts = createCollection(liveQueryCollectionOptions({ // Use the subquery in the main query return q .from({ activeUser: activeUsers }) - .join({ post: postsCollection }, ({ activeUser, post }) => + .join(leftJoin({ post: postsCollection }, ({ activeUser, post }) => eq(activeUser.id, post.userId) - ) + )) } })) ``` @@ -1155,9 +1153,9 @@ const userRecentPosts = createCollection(liveQueryCollectionOptions({ // Use the subquery in the main query return q .from({ user: usersCollection }) - .join({ recentPost: recentPosts }, ({ user, recentPost }) => + .join(leftJoin({ recentPost: recentPosts }, ({ user, recentPost }) => eq(user.id, recentPost.userId) - ) + )) } })) ``` @@ -1177,12 +1175,12 @@ const complexQuery = createCollection(liveQueryCollectionOptions({ // Use the same subquery multiple times return q .from({ activeUser: activeUsers }) - .join({ post: postsCollection }, ({ activeUser, post }) => + .join(leftJoin({ post: postsCollection }, ({ activeUser, post }) => eq(activeUser.id, post.userId) - ) - .join({ comment: commentsCollection }, ({ activeUser, comment }) => + )) + .join(leftJoin({ comment: commentsCollection }, ({ activeUser, comment }) => eq(activeUser.id, comment.userId) - ) + )) } })) ``` @@ -1194,7 +1192,7 @@ In this example, the `activeUsers` subquery is used twice but executed only once Build complex queries with multiple levels of nesting: ```ts -import { count } from '@tanstack/db' +import { count, leftJoin } from '@tanstack/db' const topUsers = createCollection(liveQueryCollectionOptions({ query: (q) => { @@ -1210,9 +1208,9 @@ const topUsers = createCollection(liveQueryCollectionOptions({ // Build the user stats subquery const userStats = q .from({ user: usersCollection }) - .join({ postCount: postCounts }, ({ user, postCount }) => + .join(leftJoin({ postCount: postCounts }, ({ user, postCount }) => eq(user.id, postCount.userId) - ) + )) .select(({ user, postCount }) => ({ id: user.id, name: user.name, @@ -1231,20 +1229,22 @@ const topUsers = createCollection(liveQueryCollectionOptions({ Includes let you nest subqueries inside `.select()` to produce hierarchical results. Instead of joins that flatten 1:N relationships into repeated rows, each parent row gets a nested collection of its related items. +Wrap a child query in `include()` to get a live child Collection, or in `toArray()` or `materialize()` to get inline values. A child query in `.select()` without one of these throws `IncludeOperatorRequiredError`. These functions bring the include code with them, so an app that never includes subqueries does not bundle it. + ```ts -import { createLiveQueryCollection, eq } from '@tanstack/db' +import { createLiveQueryCollection, eq, include } from '@tanstack/db' const projectsWithIssues = createLiveQueryCollection((q) => q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q + issues: include(q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) .select(({ i }) => ({ id: i.id, title: i.title, - })), + }))), })), ) ``` @@ -1277,7 +1277,7 @@ Child queries support additional `.where()` clauses beyond the correlation condi q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q + issues: include(q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) // correlation .where(({ i }) => eq(i.createdBy, p.createdBy)) // parent-referencing filter @@ -1285,7 +1285,7 @@ q.from({ p: projectsCollection }).select(({ p }) => ({ .select(({ i }) => ({ id: i.id, title: i.title, - })), + }))), })) ``` @@ -1299,7 +1299,7 @@ Child queries support `.orderBy()` and `.limit()`, applied per parent: q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q + issues: include(q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) .orderBy(({ i }) => i.createdAt, 'desc') @@ -1307,7 +1307,7 @@ q.from({ p: projectsCollection }).select(({ p }) => ({ .select(({ i }) => ({ id: i.id, title: i.title, - })), + }))), })) ``` @@ -1388,16 +1388,16 @@ Do not return child queries, `toArray()`, `materialize()`, or query expressions You can use aggregate functions in child queries. Aggregates are computed per parent: ```ts -import { createLiveQueryCollection, eq, count } from '@tanstack/db' +import { count, createLiveQueryCollection, eq, include } from '@tanstack/db' const projectsWithCounts = createLiveQueryCollection((q) => q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issueCount: q + issueCount: include(q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ total: count(i.id) })), + .select(({ i }) => ({ total: count(i.id) }))), })), ) ``` @@ -1413,7 +1413,7 @@ const tree = createLiveQueryCollection((q) => q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q + issues: include(q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) .select(({ i }) => ({ @@ -1426,7 +1426,7 @@ const tree = createLiveQueryCollection((q) => id: c.id, body: c.body, })), - })), + }))), })), ) ``` @@ -1439,7 +1439,7 @@ When using includes with React, each child `Collection` needs its own `useLiveQu ```tsx import { useLiveQuery } from '@tanstack/react-db' -import { eq } from '@tanstack/db' +import { eq, include } from '@tanstack/db' function ProjectList() { const { data: projects } = useLiveQuery({ @@ -1447,13 +1447,13 @@ function ProjectList() { q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q + issues: include(q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) .select(({ i }) => ({ id: i.id, title: i.title, - })), + }))), })), }) @@ -1493,6 +1493,19 @@ Each `IssueList` component independently subscribes to its project's issues. Whe Use `groupBy` to group your data and apply aggregate functions. When you use aggregates in `select` without `groupBy`, the entire result set is treated as a single group. +Aggregate functions such as `count()` bring the grouping code with them. A `groupBy` with no aggregate wraps its keys in `groupKeys()` instead; without it, the query throws `GroupingOperatorRequiredError`: + +```ts +import { createLiveQueryCollection, groupKeys } from '@tanstack/db' + +const departments = createLiveQueryCollection((q) => + q + .from({ user: usersCollection }) + .groupBy(groupKeys(({ user }) => user.departmentId)) + .select(({ user }) => ({ departmentId: user.departmentId })), +) +``` + ### Method Signature ```ts @@ -2021,6 +2034,31 @@ const page2Users = createLiveQueryCollection((q) => ) ``` +## Pooled Live Queries + +Apps that mount many live queries of the same shape at once, such as one per +list row, can share their work. Pass `pooledLiveQueries` as `pool` in a hook's +query config. Queries that filter one eager source with `eq(field, literal)` +then read one shared partition instead of compiling a graph each: + +```tsx +import { eq, pooledLiveQueries } from '@tanstack/db' +import { useLiveQuery } from '@tanstack/react-db' + +function TodoRow({ listId }: { listId: string }) { + const { data } = useLiveQuery({ + query: (q) => + q.from({ todo: todosCollection }).where(({ todo }) => eq(todo.listId, listId)), + pool: pooledLiveQueries, + }) + // ... +} +``` + +Results are the same with or without pooling. Without `pool`, each query +compiles its own live-query Collection, and the app does not bundle pooling. +Suspense hooks and queries with a `DbClient` never pool. + ## Composable Queries Build complex queries by composing smaller, reusable parts. This approach makes your queries more maintainable and allows for better performance through caching. @@ -2068,9 +2106,9 @@ const activeUsers = createLiveQueryCollection((q) => const activeUserPosts = createLiveQueryCollection((q) => q .from({ user: activeUsers }) - .join({ post: postsCollection }, ({ user, post }) => + .join(leftJoin({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId) - ) + )) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, @@ -2083,7 +2121,7 @@ const activeUserPosts = createLiveQueryCollection((q) => You can use the `Query` class to create reusable query definitions. This is useful for building complex queries where you want to reuse the same query builder instance multiple times throughout your application. ```ts -import { Query, eq } from '@tanstack/db' +import { eq, leftJoin, Query } from '@tanstack/db' // Create a reusable query builder const userQuery = new Query() @@ -2102,9 +2140,9 @@ const activeUsers = createLiveQueryCollection({ const userPosts = createLiveQueryCollection((q) => q .from({ activeUser: userQuery }) - .join({ post: postsCollection }, ({ activeUser, post }) => + .join(leftJoin({ post: postsCollection }, ({ activeUser, post }) => eq(activeUser.id, post.userId) - ) + )) ) ``` @@ -2201,9 +2239,9 @@ const highValueCustomers = createCollection(liveQueryCollectionOptions({ query: (q) => q .from({ user: usersCollection }) - .join({ order: ordersCollection }, ({ user, order }) => + .join(leftJoin({ order: ordersCollection }, ({ user, order }) => eq(user.id, order.userId) - ) + )) .where(isHighValueCustomer) .select(({ user, order }) => ({ userName: user.name, @@ -2482,9 +2520,9 @@ createEffect({ query: (q) => q .from({ user: usersCollection }) - .join({ post: postsCollection }, ({ user, post }) => + .join(leftJoin({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId) - ) + )) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, @@ -2577,16 +2615,16 @@ or hot-path queries. Here's a more complete example showing an effect that monitors order status changes and sends notifications: ```ts -import { createEffect, eq } from '@tanstack/db' +import { createEffect, eq, leftJoin } from '@tanstack/db' const orderEffect = createEffect({ id: 'order-status-monitor', query: (q) => q .from({ order: ordersCollection }) - .join({ customer: customersCollection }, ({ order, customer }) => + .join(leftJoin({ customer: customersCollection }, ({ order, customer }) => eq(order.customerId, customer.id) - ) + )) .where(({ order }) => eq(order.status, 'shipped')) .select(({ order, customer }) => ({ orderId: order.id, @@ -2673,10 +2711,9 @@ These functions are particularly important when working with joins and optional const usersWithoutProfiles = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .leftJoin( - { profile: profilesCollection }, + .join(leftJoin({ profile: profilesCollection }, ({ user, profile }) => eq(user.id, profile.userId) - ) + )) .where(({ profile }) => isUndefined(profile)) ) diff --git a/docs/overview.md b/docs/overview.md index 454afb1877..cd63288750 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -330,11 +330,8 @@ const Todos = () => { query: (q) => q .from({ todos: todoCollection }) - .join( - { lists: listCollection }, - ({ todos, lists }) => eq(lists.id, todos.listId), - 'inner' - ) + .join(innerJoin({ lists: listCollection }, + ({ todos, lists }) => eq(lists.id, todos.listId))) .where(({ lists }) => eq(lists.active, true)) .select(({ todos, lists }) => ({ id: todos.id, @@ -497,11 +494,8 @@ const Todos = () => { query: (q) => q .from({ todo: todoCollection }) - .join( - { list: listCollection }, - ({ todo, list }) => eq(list.id, todo.list_id), - "inner" - ) + .join(innerJoin({ list: listCollection }, + ({ todo, list }) => eq(list.id, todo.list_id))) .where(({ list }) => eq(list.active, true)) .select(({ todo, list }) => ({ id: todo.id, diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md b/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md index 55bd52309d..327fc833c5 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md @@ -3,7 +3,7 @@ id: Aggregate title: Aggregate --- -Defined in: [packages/db/src/query/ir.ts:260](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L260) +Defined in: [packages/db/src/query/ir.ts:274](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L274) ## Extends @@ -20,10 +20,13 @@ Defined in: [packages/db/src/query/ir.ts:260](https://github.com/TanStack/db/blo ### Constructor ```ts -new Aggregate(name, args): Aggregate; +new Aggregate( + name, + args, +grouping): Aggregate; ``` -Defined in: [packages/db/src/query/ir.ts:262](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L262) +Defined in: [packages/db/src/query/ir.ts:276](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L276) #### Parameters @@ -35,6 +38,16 @@ Defined in: [packages/db/src/query/ir.ts:262](https://github.com/TanStack/db/blo [`BasicExpression`](../type-aliases/BasicExpression.md)\<`any`\>[] +##### grouping + +###### processGroupBy + +(`pipeline`, `groupByClause`, `valueIdentity`, `isSourceRef`, `havingClauses?`, `selectClause?`, `fnHavingClauses?`, `aggregateCollectionId?`, `mainSource?`, `sanitizeCallbackRows`) => [`NamespacedAndKeyedStream`](../../../../type-aliases/NamespacedAndKeyedStream.md) + +###### replaceAggregatesByRefs + +(`havingExpr`, `selectClause`, `resultAlias`) => [`BasicExpression`](../type-aliases/BasicExpression.md) + #### Returns `Aggregate`\<`T`\> @@ -53,7 +66,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) **`Internal`** @@ -73,7 +86,114 @@ BaseExpression.__returnType args: BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:264](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L264) +Defined in: [packages/db/src/query/ir.ts:278](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L278) + +*** + +### grouping + +```ts +grouping: object; +``` + +Defined in: [packages/db/src/query/ir.ts:280](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L280) + +#### processGroupBy() + +```ts +processGroupBy: (pipeline, groupByClause, valueIdentity, isSourceRef, havingClauses?, selectClause?, fnHavingClauses?, aggregateCollectionId?, mainSource?, sanitizeCallbackRows) => NamespacedAndKeyedStream; +``` + +Processes the GROUP BY clause with optional HAVING and SELECT +Works with the new $selected structure from early SELECT processing + +##### Parameters + +###### pipeline + +[`NamespacedAndKeyedStream`](../../../../type-aliases/NamespacedAndKeyedStream.md) + +###### groupByClause + +[`GroupBy`](../type-aliases/GroupBy.md) + +###### valueIdentity + +`ValueIdentity` + +###### isSourceRef + +(`ref`) => `boolean` + +Whether a ref reads a field of the query's own rows. + +###### havingClauses? + +[`Where`](../type-aliases/Where.md)[] + +###### selectClause? + +[`Select`](../type-aliases/Select.md) + +###### fnHavingClauses? + +(`row`) => `any`[] + +###### aggregateCollectionId? + +`string` + +###### mainSource? + +`string` + +###### sanitizeCallbackRows? + +`boolean` = `false` + +##### Returns + +[`NamespacedAndKeyedStream`](../../../../type-aliases/NamespacedAndKeyedStream.md) + +#### replaceAggregatesByRefs() + +```ts +replaceAggregatesByRefs: (havingExpr, selectClause, resultAlias) => BasicExpression; +``` + +Transforms expressions to replace aggregate functions with references to computed values. + +For aggregate expressions, finds matching aggregates in the SELECT clause and replaces them +with PropRef([resultAlias, alias]) to reference the computed aggregate value. + +Ref expressions (table columns and $selected fields) and value expressions are passed through unchanged. +Function expressions are recursively transformed. + +##### Parameters + +###### havingExpr + +The expression to transform (can be aggregate, ref, func, or val) + +`Aggregate`\<`any`\> | [`BasicExpression`](../type-aliases/BasicExpression.md) + +###### selectClause + +[`Select`](../type-aliases/Select.md) + +The SELECT clause containing aliases and aggregate definitions + +###### resultAlias + +`string` = `...` + +The namespace alias for SELECT results (default: '$selected') + +##### Returns + +[`BasicExpression`](../type-aliases/BasicExpression.md) + +A transformed BasicExpression that references computed values instead of raw expressions *** @@ -83,7 +203,7 @@ Defined in: [packages/db/src/query/ir.ts:264](https://github.com/TanStack/db/blo name: string; ``` -Defined in: [packages/db/src/query/ir.ts:263](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L263) +Defined in: [packages/db/src/query/ir.ts:277](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L277) *** @@ -93,7 +213,7 @@ Defined in: [packages/db/src/query/ir.ts:263](https://github.com/TanStack/db/blo type: "agg"; ``` -Defined in: [packages/db/src/query/ir.ts:261](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L261) +Defined in: [packages/db/src/query/ir.ts:275](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L275) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md b/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md index 3d41bb5c0d..6340a15ded 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md @@ -3,7 +3,7 @@ id: CollectionRef title: CollectionRef --- -Defined in: [packages/db/src/query/ir.ts:86](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L86) +Defined in: [packages/db/src/query/ir.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L100) ## Extends @@ -20,7 +20,7 @@ new CollectionRef( bindingId?): CollectionRef; ``` -Defined in: [packages/db/src/query/ir.ts:93](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L93) +Defined in: [packages/db/src/query/ir.ts:107](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L107) #### Parameters @@ -54,7 +54,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) **`Internal`** @@ -74,7 +74,7 @@ BaseExpression.__returnType alias: string; ``` -Defined in: [packages/db/src/query/ir.ts:91](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L91) +Defined in: [packages/db/src/query/ir.ts:105](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L105) *** @@ -84,7 +84,7 @@ Defined in: [packages/db/src/query/ir.ts:91](https://github.com/TanStack/db/blob collection: CollectionImpl; ``` -Defined in: [packages/db/src/query/ir.ts:90](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L90) +Defined in: [packages/db/src/query/ir.ts:104](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L104) *** @@ -94,7 +94,7 @@ Defined in: [packages/db/src/query/ir.ts:90](https://github.com/TanStack/db/blob type: "collectionRef"; ``` -Defined in: [packages/db/src/query/ir.ts:87](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L87) +Defined in: [packages/db/src/query/ir.ts:101](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L101) #### Overrides @@ -112,7 +112,7 @@ BaseExpression.type get bindingId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:109](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L109) +Defined in: [packages/db/src/query/ir.ts:123](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L123) ##### Returns @@ -128,7 +128,7 @@ Defined in: [packages/db/src/query/ir.ts:109](https://github.com/TanStack/db/blo get sourceId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:105](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L105) +Defined in: [packages/db/src/query/ir.ts:119](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L119) Opaque runtime identity; aliases are lexical names only. diff --git a/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md b/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md index 939dbc4b16..d5ba64669d 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md @@ -3,7 +3,7 @@ id: ConditionalSelect title: ConditionalSelect --- -Defined in: [packages/db/src/query/ir.ts:304](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L304) +Defined in: [packages/db/src/query/ir.ts:322](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L322) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:304](https://github.com/TanStack/db/blo new ConditionalSelect(branches, defaultValue?): ConditionalSelect; ``` -Defined in: [packages/db/src/query/ir.ts:306](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L306) +Defined in: [packages/db/src/query/ir.ts:324](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L324) #### Parameters @@ -47,7 +47,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) **`Internal`** @@ -67,7 +67,7 @@ BaseExpression.__returnType branches: ConditionalSelectBranch[]; ``` -Defined in: [packages/db/src/query/ir.ts:307](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L307) +Defined in: [packages/db/src/query/ir.ts:325](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L325) *** @@ -77,7 +77,7 @@ Defined in: [packages/db/src/query/ir.ts:307](https://github.com/TanStack/db/blo optional defaultValue: SelectValueExpression; ``` -Defined in: [packages/db/src/query/ir.ts:308](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L308) +Defined in: [packages/db/src/query/ir.ts:326](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L326) *** @@ -87,7 +87,7 @@ Defined in: [packages/db/src/query/ir.ts:308](https://github.com/TanStack/db/blo type: "conditionalSelect"; ``` -Defined in: [packages/db/src/query/ir.ts:305](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L305) +Defined in: [packages/db/src/query/ir.ts:323](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L323) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md b/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md index ff2f45ab94..ab17414d06 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md @@ -3,7 +3,7 @@ id: DescriptorRef title: DescriptorRef --- -Defined in: [packages/db/src/query/ir.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L114) +Defined in: [packages/db/src/query/ir.ts:128](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L128) ## Extends @@ -20,7 +20,7 @@ new DescriptorRef( bindingId?): DescriptorRef; ``` -Defined in: [packages/db/src/query/ir.ts:118](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L118) +Defined in: [packages/db/src/query/ir.ts:132](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L132) #### Parameters @@ -54,7 +54,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) **`Internal`** @@ -74,7 +74,7 @@ BaseExpression.__returnType alias: string; ``` -Defined in: [packages/db/src/query/ir.ts:120](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L120) +Defined in: [packages/db/src/query/ir.ts:134](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L134) *** @@ -84,7 +84,7 @@ Defined in: [packages/db/src/query/ir.ts:120](https://github.com/TanStack/db/blo descriptor: CollectionOptionsIdentity; ``` -Defined in: [packages/db/src/query/ir.ts:119](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L119) +Defined in: [packages/db/src/query/ir.ts:133](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L133) *** @@ -94,7 +94,7 @@ Defined in: [packages/db/src/query/ir.ts:119](https://github.com/TanStack/db/blo type: "descriptorRef"; ``` -Defined in: [packages/db/src/query/ir.ts:115](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L115) +Defined in: [packages/db/src/query/ir.ts:129](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L129) #### Overrides @@ -112,7 +112,7 @@ BaseExpression.type get bindingId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:132](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L132) +Defined in: [packages/db/src/query/ir.ts:146](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L146) ##### Returns @@ -128,7 +128,7 @@ Defined in: [packages/db/src/query/ir.ts:132](https://github.com/TanStack/db/blo get sourceId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:128](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L128) +Defined in: [packages/db/src/query/ir.ts:142](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L142) Opaque runtime identity for this unbound source placement. diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Func.md b/docs/reference/@tanstack/namespaces/IR/classes/Func.md index c5c812ef86..5f0192d7df 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Func.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Func.md @@ -3,7 +3,7 @@ id: Func title: Func --- -Defined in: [packages/db/src/query/ir.ts:245](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L245) +Defined in: [packages/db/src/query/ir.ts:259](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L259) ## Extends @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/ir.ts:245](https://github.com/TanStack/db/blo new Func(name, args): Func; ``` -Defined in: [packages/db/src/query/ir.ts:247](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L247) +Defined in: [packages/db/src/query/ir.ts:261](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L261) #### Parameters @@ -53,7 +53,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) **`Internal`** @@ -73,7 +73,7 @@ BaseExpression.__returnType args: BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:249](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L249) +Defined in: [packages/db/src/query/ir.ts:263](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L263) *** @@ -83,7 +83,7 @@ Defined in: [packages/db/src/query/ir.ts:249](https://github.com/TanStack/db/blo name: string; ``` -Defined in: [packages/db/src/query/ir.ts:248](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L248) +Defined in: [packages/db/src/query/ir.ts:262](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L262) *** @@ -93,7 +93,7 @@ Defined in: [packages/db/src/query/ir.ts:248](https://github.com/TanStack/db/blo type: "func"; ``` -Defined in: [packages/db/src/query/ir.ts:246](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L246) +Defined in: [packages/db/src/query/ir.ts:260](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L260) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md b/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md index 5f519316c3..315f4695c6 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md @@ -3,7 +3,7 @@ id: IncludesSubquery title: IncludesSubquery --- -Defined in: [packages/db/src/query/ir.ts:280](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L280) +Defined in: [packages/db/src/query/ir.ts:296](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L296) ## Extends @@ -19,13 +19,14 @@ new IncludesSubquery( correlationField, childCorrelationField, fieldName, + runtime, parentFilters?, parentProjection?, materialization?, scalarField?): IncludesSubquery; ``` -Defined in: [packages/db/src/query/ir.ts:282](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L282) +Defined in: [packages/db/src/query/ir.ts:298](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L298) #### Parameters @@ -45,6 +46,16 @@ Defined in: [packages/db/src/query/ir.ts:282](https://github.com/TanStack/db/blo `string` +##### runtime + +###### createBucketFacades + +(...`args`) => `BucketFacadeAdapter` = `...` + +###### materializeCompilation + +(`compilation`, `getRootKey?`, `reduceJoinedPublicKeys`) => `MaterializedRootCompilation` + ##### parentFilters? [`Where`](../type-aliases/Where.md)[] @@ -79,7 +90,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) **`Internal`** @@ -99,7 +110,7 @@ BaseExpression.__returnType childCorrelationField: PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:285](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L285) +Defined in: [packages/db/src/query/ir.ts:301](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L301) *** @@ -109,7 +120,7 @@ Defined in: [packages/db/src/query/ir.ts:285](https://github.com/TanStack/db/blo correlationField: PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:284](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L284) +Defined in: [packages/db/src/query/ir.ts:300](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L300) *** @@ -119,7 +130,7 @@ Defined in: [packages/db/src/query/ir.ts:284](https://github.com/TanStack/db/blo fieldName: string; ``` -Defined in: [packages/db/src/query/ir.ts:286](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L286) +Defined in: [packages/db/src/query/ir.ts:302](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L302) *** @@ -129,7 +140,7 @@ Defined in: [packages/db/src/query/ir.ts:286](https://github.com/TanStack/db/blo materialization: IncludesMaterialization; ``` -Defined in: [packages/db/src/query/ir.ts:289](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L289) +Defined in: [packages/db/src/query/ir.ts:307](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L307) *** @@ -139,7 +150,7 @@ Defined in: [packages/db/src/query/ir.ts:289](https://github.com/TanStack/db/blo optional parentFilters: Where[]; ``` -Defined in: [packages/db/src/query/ir.ts:287](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L287) +Defined in: [packages/db/src/query/ir.ts:305](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L305) *** @@ -149,7 +160,7 @@ Defined in: [packages/db/src/query/ir.ts:287](https://github.com/TanStack/db/blo optional parentProjection: PropRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:288](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L288) +Defined in: [packages/db/src/query/ir.ts:306](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L306) *** @@ -159,7 +170,61 @@ Defined in: [packages/db/src/query/ir.ts:288](https://github.com/TanStack/db/blo query: QueryIR; ``` -Defined in: [packages/db/src/query/ir.ts:283](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L283) +Defined in: [packages/db/src/query/ir.ts:299](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L299) + +*** + +### runtime + +```ts +runtime: object; +``` + +Defined in: [packages/db/src/query/ir.ts:304](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L304) + +#### createBucketFacades() + +```ts +createBucketFacades: (...args) => BucketFacadeAdapter; +``` + +##### Parameters + +###### args + +...\[`string`, `BucketFacadeCompilation`[], (`count`) => `void`\] + +##### Returns + +`BucketFacadeAdapter` + +#### materializeCompilation() + +```ts +materializeCompilation: (compilation, getRootKey?, reduceJoinedPublicKeys) => MaterializedRootCompilation; +``` + +Compiles inline includes into the same D2 graph as their parent relation. +Collection-valued includes become inert bucket references. The public facade +adapter resolves those references after the graph reaches quiescence. + +##### Parameters + +###### compilation + +`CompilationResult` + +###### getRootKey? + +(`row`) => `unknown` + +###### reduceJoinedPublicKeys? + +`boolean` = `false` + +##### Returns + +`MaterializedRootCompilation` *** @@ -169,7 +234,7 @@ Defined in: [packages/db/src/query/ir.ts:283](https://github.com/TanStack/db/blo optional scalarField: string; ``` -Defined in: [packages/db/src/query/ir.ts:290](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L290) +Defined in: [packages/db/src/query/ir.ts:308](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L308) *** @@ -179,7 +244,7 @@ Defined in: [packages/db/src/query/ir.ts:290](https://github.com/TanStack/db/blo type: "includesSubquery"; ``` -Defined in: [packages/db/src/query/ir.ts:281](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L281) +Defined in: [packages/db/src/query/ir.ts:297](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L297) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md b/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md index b4f843dc13..4c374e2288 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md @@ -3,7 +3,7 @@ id: PropRef title: PropRef --- -Defined in: [packages/db/src/query/ir.ts:200](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L200) +Defined in: [packages/db/src/query/ir.ts:214](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L214) ## Extends @@ -26,7 +26,7 @@ new PropRef( bindingId?): PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:204](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L204) +Defined in: [packages/db/src/query/ir.ts:218](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L218) #### Parameters @@ -60,7 +60,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) **`Internal`** @@ -80,7 +80,7 @@ BaseExpression.__returnType readonly optional bindingId: string; ``` -Defined in: [packages/db/src/query/ir.ts:203](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L203) +Defined in: [packages/db/src/query/ir.ts:217](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L217) *** @@ -90,7 +90,7 @@ Defined in: [packages/db/src/query/ir.ts:203](https://github.com/TanStack/db/blo path: string[]; ``` -Defined in: [packages/db/src/query/ir.ts:205](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L205) +Defined in: [packages/db/src/query/ir.ts:219](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L219) *** @@ -100,7 +100,7 @@ Defined in: [packages/db/src/query/ir.ts:205](https://github.com/TanStack/db/blo readonly optional sourceAlias: string; ``` -Defined in: [packages/db/src/query/ir.ts:202](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L202) +Defined in: [packages/db/src/query/ir.ts:216](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L216) *** @@ -110,7 +110,7 @@ Defined in: [packages/db/src/query/ir.ts:202](https://github.com/TanStack/db/blo type: "ref"; ``` -Defined in: [packages/db/src/query/ir.ts:201](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L201) +Defined in: [packages/db/src/query/ir.ts:215](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L215) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md b/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md index 9050a6ba10..78100fc3e9 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md @@ -3,7 +3,7 @@ id: QueryRef title: QueryRef --- -Defined in: [packages/db/src/query/ir.ts:155](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L155) +Defined in: [packages/db/src/query/ir.ts:169](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L169) ## Extends @@ -20,7 +20,7 @@ new QueryRef( bindingId?): QueryRef; ``` -Defined in: [packages/db/src/query/ir.ts:158](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L158) +Defined in: [packages/db/src/query/ir.ts:172](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L172) #### Parameters @@ -54,7 +54,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) **`Internal`** @@ -74,7 +74,7 @@ BaseExpression.__returnType alias: string; ``` -Defined in: [packages/db/src/query/ir.ts:160](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L160) +Defined in: [packages/db/src/query/ir.ts:174](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L174) *** @@ -84,7 +84,7 @@ Defined in: [packages/db/src/query/ir.ts:160](https://github.com/TanStack/db/blo query: QueryIR; ``` -Defined in: [packages/db/src/query/ir.ts:159](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L159) +Defined in: [packages/db/src/query/ir.ts:173](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L173) *** @@ -94,7 +94,7 @@ Defined in: [packages/db/src/query/ir.ts:159](https://github.com/TanStack/db/blo type: "queryRef"; ``` -Defined in: [packages/db/src/query/ir.ts:156](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L156) +Defined in: [packages/db/src/query/ir.ts:170](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L170) #### Overrides @@ -112,7 +112,7 @@ BaseExpression.type get bindingId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:167](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L167) +Defined in: [packages/db/src/query/ir.ts:181](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L181) ##### Returns diff --git a/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md b/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md index 88c347b38d..94597bc287 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md @@ -3,7 +3,7 @@ id: UnionAll title: UnionAll --- -Defined in: [packages/db/src/query/ir.ts:183](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L183) +Defined in: [packages/db/src/query/ir.ts:197](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L197) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:183](https://github.com/TanStack/db/blo new UnionAll(queries): UnionAll; ``` -Defined in: [packages/db/src/query/ir.ts:191](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L191) +Defined in: [packages/db/src/query/ir.ts:205](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L205) Result-level UNION ALL. Downstream query clauses see the union result row shape, not the branch source aliases. Optimizers may push safe operations @@ -48,7 +48,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) **`Internal`** @@ -68,7 +68,7 @@ BaseExpression.__returnType queries: QueryIR[]; ``` -Defined in: [packages/db/src/query/ir.ts:191](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L191) +Defined in: [packages/db/src/query/ir.ts:205](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L205) *** @@ -78,7 +78,7 @@ Defined in: [packages/db/src/query/ir.ts:191](https://github.com/TanStack/db/blo type: "unionAll"; ``` -Defined in: [packages/db/src/query/ir.ts:184](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L184) +Defined in: [packages/db/src/query/ir.ts:198](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L198) #### Overrides @@ -96,7 +96,7 @@ BaseExpression.type get alias(): string; ``` -Defined in: [packages/db/src/query/ir.ts:195](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L195) +Defined in: [packages/db/src/query/ir.ts:209](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L209) ##### Returns diff --git a/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md b/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md index 53204f94e7..c5a3aac729 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md @@ -3,7 +3,7 @@ id: UnionFrom title: UnionFrom --- -Defined in: [packages/db/src/query/ir.ts:172](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L172) +Defined in: [packages/db/src/query/ir.ts:186](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L186) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:172](https://github.com/TanStack/db/blo new UnionFrom(sources): UnionFrom; ``` -Defined in: [packages/db/src/query/ir.ts:174](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L174) +Defined in: [packages/db/src/query/ir.ts:188](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L188) #### Parameters @@ -45,7 +45,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) **`Internal`** @@ -67,7 +67,7 @@ sources: ( | QueryRef)[]; ``` -Defined in: [packages/db/src/query/ir.ts:174](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L174) +Defined in: [packages/db/src/query/ir.ts:188](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L188) *** @@ -77,7 +77,7 @@ Defined in: [packages/db/src/query/ir.ts:174](https://github.com/TanStack/db/blo type: "unionFrom"; ``` -Defined in: [packages/db/src/query/ir.ts:173](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L173) +Defined in: [packages/db/src/query/ir.ts:187](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L187) #### Overrides @@ -95,7 +95,7 @@ BaseExpression.type get alias(): string; ``` -Defined in: [packages/db/src/query/ir.ts:178](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L178) +Defined in: [packages/db/src/query/ir.ts:192](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L192) ##### Returns diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Value.md b/docs/reference/@tanstack/namespaces/IR/classes/Value.md index 5f9eee3388..60029193cf 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Value.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Value.md @@ -3,7 +3,7 @@ id: Value title: Value --- -Defined in: [packages/db/src/query/ir.ts:236](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L236) +Defined in: [packages/db/src/query/ir.ts:250](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L250) ## Extends @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/ir.ts:236](https://github.com/TanStack/db/blo new Value(value): Value; ``` -Defined in: [packages/db/src/query/ir.ts:238](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L238) +Defined in: [packages/db/src/query/ir.ts:252](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L252) #### Parameters @@ -49,7 +49,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) **`Internal`** @@ -69,7 +69,7 @@ BaseExpression.__returnType type: "val"; ``` -Defined in: [packages/db/src/query/ir.ts:237](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L237) +Defined in: [packages/db/src/query/ir.ts:251](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L251) #### Overrides @@ -85,4 +85,4 @@ BaseExpression.type value: T; ``` -Defined in: [packages/db/src/query/ir.ts:239](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L239) +Defined in: [packages/db/src/query/ir.ts:253](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L253) diff --git a/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md b/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md index 9ce541176e..1f31eae7df 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md @@ -7,7 +7,7 @@ title: collectCollectionSources function collectCollectionSources(query): CollectionRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:384](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L384) +Defined in: [packages/db/src/query/ir.ts:402](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L402) Returns concrete Collection sources after requiring client binding. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md b/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md index 77985cc3c3..e74e70c870 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md @@ -7,7 +7,7 @@ title: collectPropRefs function collectPropRefs(expression): PropRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:271](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L271) +Defined in: [packages/db/src/query/ir.ts:287](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L287) Collect refs from an IR expression, including aggregate arguments. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md b/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md index 586bd48327..b9cc577395 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md @@ -7,7 +7,7 @@ title: collectSourceRefs function collectSourceRefs(query): CollectionSourceRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:335](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L335) +Defined in: [packages/db/src/query/ir.ts:353](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L353) Returns each lexical Collection or descriptor source in a query tree once. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md b/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md index 048dc97d13..c5fd381f4b 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md @@ -7,7 +7,7 @@ title: createResidualWhere function createResidualWhere(expression): Where; ``` -Defined in: [packages/db/src/query/ir.ts:429](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L429) +Defined in: [packages/db/src/query/ir.ts:447](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L447) Create a residual Where clause from an expression diff --git a/docs/reference/@tanstack/namespaces/IR/functions/followRef.md b/docs/reference/@tanstack/namespaces/IR/functions/followRef.md index 9dacda480c..64340b8a4e 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/followRef.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/followRef.md @@ -17,7 +17,7 @@ function followRef( }; ``` -Defined in: [packages/db/src/query/ir.ts:477](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L477) +Defined in: [packages/db/src/query/ir.ts:495](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L495) Follows the given reference in a query until its finds the root field the reference points to. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md b/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md index c1c20db12c..d31d0d432e 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md @@ -9,7 +9,7 @@ function getFromSources(from): ( | QueryRef)[]; ``` -Defined in: [packages/db/src/query/ir.ts:436](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L436) +Defined in: [packages/db/src/query/ir.ts:454](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L454) Sources declared by a FROM clause. UnionAll branches own their sources. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md b/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md index ad73a98147..5c5ec25f26 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md @@ -9,7 +9,7 @@ function getHavingExpression(having): | Aggregate; ``` -Defined in: [packages/db/src/query/ir.ts:407](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L407) +Defined in: [packages/db/src/query/ir.ts:425](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L425) Extract the expression from a HAVING clause HAVING clauses can contain aggregates, unlike regular WHERE clauses diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md index 0a0a321607..556f77b80e 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md @@ -7,7 +7,7 @@ title: getPropRefPropertyPath function getPropRefPropertyPath(ref): string[]; ``` -Defined in: [packages/db/src/query/ir.ts:232](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L232) +Defined in: [packages/db/src/query/ir.ts:246](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L246) Returns the property path after removing only explicit source qualification. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md index 5f0cffd844..6cc8981db0 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md @@ -7,7 +7,7 @@ title: getPropRefSourceAlias function getPropRefSourceAlias(ref): string | undefined; ``` -Defined in: [packages/db/src/query/ir.ts:225](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L225) +Defined in: [packages/db/src/query/ir.ts:239](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L239) Returns an explicitly declared source alias without inferring from the path. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md b/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md index 085fe9e367..773bff4a40 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md @@ -7,7 +7,7 @@ title: getWhereExpression function getWhereExpression(where): BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:397](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L397) +Defined in: [packages/db/src/query/ir.ts:415](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L415) Extract the expression from a Where clause diff --git a/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md b/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md index c2b0987c8b..2b3ffc0c0a 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md @@ -7,7 +7,7 @@ title: isBasicOrAggregateExpression function isBasicOrAggregateExpression(value): value is BasicExpression | Aggregate; ``` -Defined in: [packages/db/src/query/ir.ts:315](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L315) +Defined in: [packages/db/src/query/ir.ts:333](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L333) Distinguish compiler expressions from user objects with IR-like fields. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md b/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md index ade97402d8..d5fc5d8c1b 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md @@ -7,7 +7,7 @@ title: isExpressionLike function isExpressionLike(value): boolean; ``` -Defined in: [packages/db/src/query/ir.ts:326](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L326) +Defined in: [packages/db/src/query/ir.ts:344](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L344) ## Parameters diff --git a/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md b/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md index c350afc596..1ebfef7204 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md @@ -7,7 +7,7 @@ title: isResidualWhere function isResidualWhere(where): boolean; ``` -Defined in: [packages/db/src/query/ir.ts:418](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L418) +Defined in: [packages/db/src/query/ir.ts:436](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L436) Check if a Where clause is marked as residual diff --git a/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md b/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md index d0fdee7500..01a9961fcc 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md @@ -9,7 +9,7 @@ title: requireCollectionSource function requireCollectionSource(source): never; ``` -Defined in: [packages/db/src/query/ir.ts:138](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L138) +Defined in: [packages/db/src/query/ir.ts:152](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L152) A descriptor must bind to a DbClient before a Collection is required. @@ -29,7 +29,7 @@ A descriptor must bind to a DbClient before a Collection is required. function requireCollectionSource(source): CollectionImpl; ``` -Defined in: [packages/db/src/query/ir.ts:139](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L139) +Defined in: [packages/db/src/query/ir.ts:153](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L153) A descriptor must bind to a DbClient before a Collection is required. diff --git a/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md b/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md index 133eedad18..0e67e96bd1 100644 --- a/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md +++ b/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md @@ -3,10 +3,121 @@ id: JoinClause title: JoinClause --- -Defined in: [packages/db/src/query/ir.ts:48](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L48) +Defined in: [packages/db/src/query/ir.ts:56](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L56) ## Properties +### compile() + +```ts +compile: (pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, localFrom, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, valueIdentity, parentKeyStream?) => NamespacedAndKeyedStream; +``` + +Defined in: [packages/db/src/query/ir.ts:62](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L62) + +Processes all join clauses, applying lazy loading optimizations and maintaining +alias tracking for per-alias subscriptions (enables self-joins). + +#### Parameters + +##### pipeline + +[`NamespacedAndKeyedStream`](../../../../type-aliases/NamespacedAndKeyedStream.md) + +##### joinClauses + +`JoinClause`[] + +##### sources + +`Record`\<`string`, [`KeyedStream`](../../../../type-aliases/KeyedStream.md)\> + +##### mainCollectionId + +`string` + +##### mainSource + +`string` + +##### allInputs + +`Record`\<`string`, [`KeyedStream`](../../../../type-aliases/KeyedStream.md)\> + +##### cache + +`QueryCache` + +##### queryMapping + +`QueryMapping` + +##### collections + +`Record`\<`string`, [`Collection`](../../../../interfaces/Collection.md)\> + +##### subscriptions + +`Record`\<`string`, `CollectionSubscription`\> + +##### callbacks + +`Record`\<`string`, `LazyCollectionCallbacks`\> + +##### lazySources + +`Set`\<`string`\> + +##### optimizableOrderByCollections + +`Record`\<`string`, `OrderByOptimizationInfo`\> + +##### setWindowFn + +(`windowFn`) => `void` + +##### rawQuery + +[`QueryIR`](QueryIR.md) + +##### localFrom + +[`From`](../type-aliases/From.md) + +##### onCompileSubquery + +(...`args`) => `CompilationResult` + +##### aliasToCollectionId + +`Record`\<`string`, `string`\> + +##### aliasRemapping + +`Record`\<`string`, `string`\> + +##### sourceWhereClauses + +`Map`\<`string`, [`BasicExpression`](../type-aliases/BasicExpression.md)\<`boolean`\>\> + +##### mainSourceIsParentFiltered + +`boolean` + +##### valueIdentity + +`ValueIdentity` + +##### parentKeyStream? + +[`KeyedStream`](../../../../type-aliases/KeyedStream.md) + +#### Returns + +[`NamespacedAndKeyedStream`](../../../../type-aliases/NamespacedAndKeyedStream.md) + +*** + ### from ```ts @@ -15,7 +126,39 @@ from: | QueryRef; ``` -Defined in: [packages/db/src/query/ir.ts:49](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L49) +Defined in: [packages/db/src/query/ir.ts:57](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L57) + +*** + +### materialize() + +```ts +materialize: (compilation, getRootKey?, reduceJoinedPublicKeys) => MaterializedRootCompilation; +``` + +Defined in: [packages/db/src/query/ir.ts:65](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L65) + +Compiles inline includes into the same D2 graph as their parent relation. +Collection-valued includes become inert bucket references. The public facade +adapter resolves those references after the graph reaches quiescence. + +#### Parameters + +##### compilation + +`CompilationResult` + +##### getRootKey? + +(`row`) => `unknown` + +##### reduceJoinedPublicKeys? + +`boolean` = `false` + +#### Returns + +`MaterializedRootCompilation` *** @@ -25,7 +168,43 @@ Defined in: [packages/db/src/query/ir.ts:49](https://github.com/TanStack/db/blob on: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:51](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L51) +Defined in: [packages/db/src/query/ir.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L59) + +*** + +### optimize() + +```ts +optimize: (query, groupedClauses) => QueryIR; +``` + +Defined in: [packages/db/src/query/ir.ts:63](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L63) + +Step 4: Apply optimizations by lifting single-source clauses into subqueries. + +Creates a new QueryIR with single-source WHERE clauses moved to subqueries +that wrap the original table references. This ensures immutability and prevents +infinite recursion issues. + +#### Parameters + +##### query + +[`QueryIR`](QueryIR.md) + +Original QueryIR to optimize + +##### groupedClauses + +`GroupedWhereClauses` + +WHERE clauses grouped by optimization strategy + +#### Returns + +[`QueryIR`](QueryIR.md) + +New QueryIR with optimizations applied *** @@ -35,4 +214,4 @@ Defined in: [packages/db/src/query/ir.ts:51](https://github.com/TanStack/db/blob type: "inner" | "left" | "right" | "full" | "outer" | "cross"; ``` -Defined in: [packages/db/src/query/ir.ts:50](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L50) +Defined in: [packages/db/src/query/ir.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L58) diff --git a/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md b/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md index 9508a24e38..08b31d931c 100644 --- a/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md +++ b/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md @@ -3,7 +3,7 @@ id: QueryIR title: QueryIR --- -Defined in: [packages/db/src/query/ir.ts:13](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L13) +Defined in: [packages/db/src/query/ir.ts:19](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L19) ## Properties @@ -13,7 +13,7 @@ Defined in: [packages/db/src/query/ir.ts:13](https://github.com/TanStack/db/blob optional distinct: true; ``` -Defined in: [packages/db/src/query/ir.ts:23](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L23) +Defined in: [packages/db/src/query/ir.ts:31](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L31) *** @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/ir.ts:23](https://github.com/TanStack/db/blob optional fnHaving: (row) => any[]; ``` -Defined in: [packages/db/src/query/ir.ts:29](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L29) +Defined in: [packages/db/src/query/ir.ts:37](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L37) #### Parameters @@ -43,7 +43,7 @@ Defined in: [packages/db/src/query/ir.ts:29](https://github.com/TanStack/db/blob optional fnSelect: (row) => any; ``` -Defined in: [packages/db/src/query/ir.ts:27](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L27) +Defined in: [packages/db/src/query/ir.ts:35](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L35) #### Parameters @@ -63,7 +63,7 @@ Defined in: [packages/db/src/query/ir.ts:27](https://github.com/TanStack/db/blob optional fnWhere: (row) => any[]; ``` -Defined in: [packages/db/src/query/ir.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L28) +Defined in: [packages/db/src/query/ir.ts:36](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L36) #### Parameters @@ -83,7 +83,7 @@ Defined in: [packages/db/src/query/ir.ts:28](https://github.com/TanStack/db/blob from: From; ``` -Defined in: [packages/db/src/query/ir.ts:14](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L14) +Defined in: [packages/db/src/query/ir.ts:20](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L20) *** @@ -93,7 +93,114 @@ Defined in: [packages/db/src/query/ir.ts:14](https://github.com/TanStack/db/blob optional groupBy: GroupBy; ``` -Defined in: [packages/db/src/query/ir.ts:18](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L18) +Defined in: [packages/db/src/query/ir.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L24) + +*** + +### grouping? + +```ts +optional grouping: object; +``` + +Defined in: [packages/db/src/query/ir.ts:26](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L26) + +#### processGroupBy() + +```ts +processGroupBy: (pipeline, groupByClause, valueIdentity, isSourceRef, havingClauses?, selectClause?, fnHavingClauses?, aggregateCollectionId?, mainSource?, sanitizeCallbackRows) => NamespacedAndKeyedStream; +``` + +Processes the GROUP BY clause with optional HAVING and SELECT +Works with the new $selected structure from early SELECT processing + +##### Parameters + +###### pipeline + +[`NamespacedAndKeyedStream`](../../../../type-aliases/NamespacedAndKeyedStream.md) + +###### groupByClause + +[`GroupBy`](../type-aliases/GroupBy.md) + +###### valueIdentity + +`ValueIdentity` + +###### isSourceRef + +(`ref`) => `boolean` + +Whether a ref reads a field of the query's own rows. + +###### havingClauses? + +[`Where`](../type-aliases/Where.md)[] + +###### selectClause? + +[`Select`](../type-aliases/Select.md) + +###### fnHavingClauses? + +(`row`) => `any`[] + +###### aggregateCollectionId? + +`string` + +###### mainSource? + +`string` + +###### sanitizeCallbackRows? + +`boolean` = `false` + +##### Returns + +[`NamespacedAndKeyedStream`](../../../../type-aliases/NamespacedAndKeyedStream.md) + +#### replaceAggregatesByRefs() + +```ts +replaceAggregatesByRefs: (havingExpr, selectClause, resultAlias) => BasicExpression; +``` + +Transforms expressions to replace aggregate functions with references to computed values. + +For aggregate expressions, finds matching aggregates in the SELECT clause and replaces them +with PropRef([resultAlias, alias]) to reference the computed aggregate value. + +Ref expressions (table columns and $selected fields) and value expressions are passed through unchanged. +Function expressions are recursively transformed. + +##### Parameters + +###### havingExpr + +The expression to transform (can be aggregate, ref, func, or val) + +[`Aggregate`](../classes/Aggregate.md)\<`any`\> | [`BasicExpression`](../type-aliases/BasicExpression.md) + +###### selectClause + +[`Select`](../type-aliases/Select.md) + +The SELECT clause containing aliases and aggregate definitions + +###### resultAlias + +`string` = `...` + +The namespace alias for SELECT results (default: '$selected') + +##### Returns + +[`BasicExpression`](../type-aliases/BasicExpression.md) + +A transformed BasicExpression that references computed values instead of raw expressions *** @@ -103,7 +210,7 @@ Defined in: [packages/db/src/query/ir.ts:18](https://github.com/TanStack/db/blob optional having: Where[]; ``` -Defined in: [packages/db/src/query/ir.ts:19](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L19) +Defined in: [packages/db/src/query/ir.ts:27](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L27) *** @@ -113,7 +220,7 @@ Defined in: [packages/db/src/query/ir.ts:19](https://github.com/TanStack/db/blob optional join: Join; ``` -Defined in: [packages/db/src/query/ir.ts:16](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L16) +Defined in: [packages/db/src/query/ir.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L22) *** @@ -123,7 +230,7 @@ Defined in: [packages/db/src/query/ir.ts:16](https://github.com/TanStack/db/blob optional limit: number; ``` -Defined in: [packages/db/src/query/ir.ts:21](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L21) +Defined in: [packages/db/src/query/ir.ts:29](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L29) *** @@ -133,7 +240,7 @@ Defined in: [packages/db/src/query/ir.ts:21](https://github.com/TanStack/db/blob optional offset: number; ``` -Defined in: [packages/db/src/query/ir.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L22) +Defined in: [packages/db/src/query/ir.ts:30](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L30) *** @@ -143,7 +250,7 @@ Defined in: [packages/db/src/query/ir.ts:22](https://github.com/TanStack/db/blob optional orderBy: OrderBy; ``` -Defined in: [packages/db/src/query/ir.ts:20](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L20) +Defined in: [packages/db/src/query/ir.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L28) *** @@ -153,7 +260,7 @@ Defined in: [packages/db/src/query/ir.ts:20](https://github.com/TanStack/db/blob optional select: Select; ``` -Defined in: [packages/db/src/query/ir.ts:15](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L15) +Defined in: [packages/db/src/query/ir.ts:21](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L21) *** @@ -163,7 +270,7 @@ Defined in: [packages/db/src/query/ir.ts:15](https://github.com/TanStack/db/blob optional singleResult: true; ``` -Defined in: [packages/db/src/query/ir.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L24) +Defined in: [packages/db/src/query/ir.ts:32](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L32) *** @@ -173,4 +280,4 @@ Defined in: [packages/db/src/query/ir.ts:24](https://github.com/TanStack/db/blob optional where: Where[]; ``` -Defined in: [packages/db/src/query/ir.ts:17](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L17) +Defined in: [packages/db/src/query/ir.ts:23](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L23) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md index 93addc9dd4..c362542670 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md @@ -10,7 +10,7 @@ type BasicExpression = | Func; ``` -Defined in: [packages/db/src/query/ir.ts:258](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L258) +Defined in: [packages/db/src/query/ir.ts:272](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L272) ## Type Parameters diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md index 463ab615cc..27a23d1865 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md @@ -9,4 +9,4 @@ type CollectionSourceRef = | DescriptorRef; ``` -Defined in: [packages/db/src/query/ir.ts:37](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L37) +Defined in: [packages/db/src/query/ir.ts:45](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L45) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md index b377881258..57444c9ab0 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md @@ -7,7 +7,7 @@ title: ConditionalSelectBranch type ConditionalSelectBranch = object; ``` -Defined in: [packages/db/src/query/ir.ts:296](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L296) +Defined in: [packages/db/src/query/ir.ts:314](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L314) ## Properties @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:296](https://github.com/TanStack/db/blo condition: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:297](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L297) +Defined in: [packages/db/src/query/ir.ts:315](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L315) *** @@ -27,4 +27,4 @@ Defined in: [packages/db/src/query/ir.ts:297](https://github.com/TanStack/db/blo value: SelectValueExpression; ``` -Defined in: [packages/db/src/query/ir.ts:298](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L298) +Defined in: [packages/db/src/query/ir.ts:316](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L316) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md index 36ed7c1405..2a823b3686 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md @@ -11,4 +11,4 @@ type From = | UnionAll; ``` -Defined in: [packages/db/src/query/ir.ts:39](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L39) +Defined in: [packages/db/src/query/ir.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L47) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md index 04b5f782e8..39fcb7dfae 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md @@ -7,4 +7,4 @@ title: GroupBy type GroupBy = BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L58) +Defined in: [packages/db/src/query/ir.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L72) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md index 3b3c1de2bd..e785f66252 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md @@ -7,4 +7,4 @@ title: Having type Having = Where; ``` -Defined in: [packages/db/src/query/ir.ts:60](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L60) +Defined in: [packages/db/src/query/ir.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L74) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md index 59f0a02e51..9493ebb62e 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md @@ -7,4 +7,4 @@ title: IncludesMaterialization type IncludesMaterialization = "collection" | "array" | "singleton" | "concat"; ``` -Defined in: [packages/db/src/query/ir.ts:32](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L32) +Defined in: [packages/db/src/query/ir.ts:40](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L40) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md index 66184f5b49..098a1f2af4 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md @@ -7,4 +7,4 @@ title: Join type Join = JoinClause[]; ``` -Defined in: [packages/db/src/query/ir.ts:46](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L46) +Defined in: [packages/db/src/query/ir.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L54) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md index 75fa827380..ba7d92188d 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md @@ -7,4 +7,4 @@ title: Limit type Limit = number; ``` -Defined in: [packages/db/src/query/ir.ts:71](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L71) +Defined in: [packages/db/src/query/ir.ts:85](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L85) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md index 7ad44b2d52..1cd8beb575 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md @@ -7,4 +7,4 @@ title: Offset type Offset = number; ``` -Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L73) +Defined in: [packages/db/src/query/ir.ts:87](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L87) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md index ddf3008f96..73d4d8137f 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md @@ -7,4 +7,4 @@ title: OrderBy type OrderBy = OrderByClause[]; ``` -Defined in: [packages/db/src/query/ir.ts:62](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L62) +Defined in: [packages/db/src/query/ir.ts:76](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L76) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md index b133297862..f0be0829f4 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md @@ -7,7 +7,7 @@ title: OrderByClause type OrderByClause = object; ``` -Defined in: [packages/db/src/query/ir.ts:64](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L64) +Defined in: [packages/db/src/query/ir.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L78) ## Properties @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:64](https://github.com/TanStack/db/blob compareOptions: CompareOptions; ``` -Defined in: [packages/db/src/query/ir.ts:66](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L66) +Defined in: [packages/db/src/query/ir.ts:80](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L80) *** @@ -27,4 +27,4 @@ Defined in: [packages/db/src/query/ir.ts:66](https://github.com/TanStack/db/blob expression: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:65](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L65) +Defined in: [packages/db/src/query/ir.ts:79](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L79) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md index 7ce4889052..77d0dc3d6e 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md @@ -7,4 +7,4 @@ title: OrderByDirection type OrderByDirection = "asc" | "desc"; ``` -Defined in: [packages/db/src/query/ir.ts:69](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L69) +Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md index 21efa898ba..b801434c9e 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md @@ -7,7 +7,7 @@ title: Select type Select = object; ``` -Defined in: [packages/db/src/query/ir.ts:41](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L41) +Defined in: [packages/db/src/query/ir.ts:49](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L49) ## Index Signature diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md index 85c9a9c872..8cbebc9a43 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md @@ -12,4 +12,4 @@ type SelectValueExpression = | ConditionalSelect; ``` -Defined in: [packages/db/src/query/ir.ts:301](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L301) +Defined in: [packages/db/src/query/ir.ts:319](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L319) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md index 87240f0c49..8baa8ea90e 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md @@ -12,4 +12,4 @@ type Where = }; ``` -Defined in: [packages/db/src/query/ir.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L54) +Defined in: [packages/db/src/query/ir.ts:68](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L68) diff --git a/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md b/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md index a19de28691..99b98a72c4 100644 --- a/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md +++ b/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md @@ -7,4 +7,4 @@ title: INCLUDES_SCALAR_FIELD const INCLUDES_SCALAR_FIELD: "__includes_scalar__"; ``` -Defined in: [packages/db/src/query/ir.ts:35](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L35) +Defined in: [packages/db/src/query/ir.ts:43](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L43) diff --git a/docs/reference/classes/AggregateFunctionNotInSelectError.md b/docs/reference/classes/AggregateFunctionNotInSelectError.md index 54dbc5a00f..3297eaa318 100644 --- a/docs/reference/classes/AggregateFunctionNotInSelectError.md +++ b/docs/reference/classes/AggregateFunctionNotInSelectError.md @@ -3,7 +3,7 @@ id: AggregateFunctionNotInSelectError title: AggregateFunctionNotInSelectError --- -Defined in: [packages/db/src/errors.ts:967](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L967) +Defined in: [packages/db/src/errors.ts:997](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L997) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:967](https://github.com/TanStack/db/blob/ new AggregateFunctionNotInSelectError(functionName): AggregateFunctionNotInSelectError; ``` -Defined in: [packages/db/src/errors.ts:968](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L968) +Defined in: [packages/db/src/errors.ts:998](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L998) #### Parameters diff --git a/docs/reference/classes/BaseQueryBuilder.md b/docs/reference/classes/BaseQueryBuilder.md index 0b189533d2..df243b9da6 100644 --- a/docs/reference/classes/BaseQueryBuilder.md +++ b/docs/reference/classes/BaseQueryBuilder.md @@ -3,7 +3,7 @@ id: BaseQueryBuilder title: BaseQueryBuilder --- -Defined in: [packages/db/src/query/builder/index.ts:138](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L138) +Defined in: [packages/db/src/query/builder/index.ts:142](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L142) ## Type Parameters @@ -19,7 +19,7 @@ Defined in: [packages/db/src/query/builder/index.ts:138](https://github.com/TanS new BaseQueryBuilder(query, owned): BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:141](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L141) +Defined in: [packages/db/src/query/builder/index.ts:145](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L145) #### Parameters @@ -49,7 +49,7 @@ Whether the builder may keep `query` without copying it. get fn(): object; ``` -Defined in: [packages/db/src/query/builder/index.ts:946](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L946) +Defined in: [packages/db/src/query/builder/index.ts:862](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L862) Functional variants of the query builder These are imperative function that are called for ery row. @@ -192,7 +192,7 @@ query _bindCollectionSources(resolveCollection): BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:1051](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1051) +Defined in: [packages/db/src/query/builder/index.ts:967](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L967) Bind this query for one consumer. Later descriptor clauses wait for another consumer. @@ -214,7 +214,7 @@ Bind this query for one consumer. Later descriptor clauses wait for another cons _getQuery(): QueryIR; ``` -Defined in: [packages/db/src/query/builder/index.ts:1043](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1043) +Defined in: [packages/db/src/query/builder/index.ts:959](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L959) #### Returns @@ -228,7 +228,7 @@ Defined in: [packages/db/src/query/builder/index.ts:1043](https://github.com/Tan distinct(): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:863](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L863) +Defined in: [packages/db/src/query/builder/index.ts:779](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L779) Specify that the query should return distinct rows. Deduplicates rows based on the selected columns. @@ -257,7 +257,7 @@ query findOne(): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:883](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L883) +Defined in: [packages/db/src/query/builder/index.ts:799](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L799) Specify that the query should return a single result @@ -285,7 +285,7 @@ query from(source): QueryBuilder>; ``` -Defined in: [packages/db/src/query/builder/index.ts:248](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L248) +Defined in: [packages/db/src/query/builder/index.ts:252](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L252) Specify the source table or subquery for the query @@ -322,70 +322,21 @@ query.from({ activeUsers }) *** -### fullJoin() - -```ts -fullJoin(source, onCallback): QueryBuilder, "full">>; -``` - -Defined in: [packages/db/src/query/builder/index.ts:468](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L468) - -Perform a FULL JOIN with another table or subquery - -#### Type Parameters - -##### TSource - -`TSource` *extends* [`Source`](../type-aliases/Source.md) - -#### Parameters - -##### source - -`TSource` - -An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery - -##### onCallback - -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> - -A function that receives table references and returns the join condition - -#### Returns - -[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`MergeContextWithJoinType`](../type-aliases/MergeContextWithJoinType.md)\<`TContext`, [`SchemaFromSource`](../type-aliases/SchemaFromSource.md)\<`TSource`\>, `"full"`\>\> - -A QueryBuilder with the full joined table available - -#### Example - -```ts -// Full join users with posts -query - .from({ users: usersCollection }) - .fullJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) -``` - -*** - ### groupBy() ```ts -groupBy(callback): QueryBuilder; +groupBy(keys): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:782](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L782) +Defined in: [packages/db/src/query/builder/index.ts:694](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L694) Group rows by one or more columns for aggregation #### Parameters -##### callback - -[`GroupByCallback`](../type-aliases/GroupByCallback.md)\<`TContext`\> +##### keys -A function that receives table references and returns the field(s) to group by +[`GroupByCallback`](../type-aliases/GroupByCallback.md)\<`TContext`\> | [`GroupKeys`](../interfaces/GroupKeys.md)\<`TContext`\> #### Returns @@ -424,7 +375,7 @@ query having(callback): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:565](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L565) +Defined in: [packages/db/src/query/builder/index.ts:477](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L477) Filter grouped rows based on aggregate conditions @@ -467,63 +418,13 @@ query *** -### innerJoin() - -```ts -innerJoin(source, onCallback): QueryBuilder, "inner">>; -``` - -Defined in: [packages/db/src/query/builder/index.ts:442](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L442) - -Perform an INNER JOIN with another table or subquery - -#### Type Parameters - -##### TSource - -`TSource` *extends* [`Source`](../type-aliases/Source.md) - -#### Parameters - -##### source - -`TSource` - -An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery - -##### onCallback - -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> - -A function that receives table references and returns the join condition - -#### Returns - -[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`MergeContextWithJoinType`](../type-aliases/MergeContextWithJoinType.md)\<`TContext`, [`SchemaFromSource`](../type-aliases/SchemaFromSource.md)\<`TSource`\>, `"inner"`\>\> - -A QueryBuilder with the inner joined table available - -#### Example - -```ts -// Inner join users with posts -query - .from({ users: usersCollection }) - .innerJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) -``` - -*** - ### join() ```ts -join( - source, - onCallback, -type): QueryBuilder, TJoinType>>; +join(operator): QueryBuilder, TJoinType>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:337](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L337) +Defined in: [packages/db/src/query/builder/index.ts:340](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L340) Join another table or subquery to the current query @@ -535,27 +436,13 @@ Join another table or subquery to the current query ##### TJoinType -`TJoinType` *extends* `"inner"` \| `"left"` \| `"right"` \| `"full"` = `"left"` +`TJoinType` *extends* [`JoinType`](../type-aliases/JoinType.md) #### Parameters -##### source - -`TSource` - -An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery - -##### onCallback +##### operator -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> - -A function that receives table references and returns the join condition - -##### type - -`TJoinType` = `...` - -The type of join: 'inner', 'left', 'right', or 'full' (defaults to 'left') +[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `TJoinType`\> #### Returns @@ -569,65 +456,17 @@ A QueryBuilder with the joined table available // Left join users with posts query .from({ users: usersCollection }) - .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) + .join(leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))) -// Inner join with explicit type +// Inner join query .from({ u: usersCollection }) - .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId), 'inner') -``` + .join(innerJoin({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId))) -// Join with a subquery -const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active) +// Join type chosen at run time query - .from({ activeUsers }) - .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId)) - -*** - -### leftJoin() - -```ts -leftJoin(source, onCallback): QueryBuilder, "left">>; -``` - -Defined in: [packages/db/src/query/builder/index.ts:390](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L390) - -Perform a LEFT JOIN with another table or subquery - -#### Type Parameters - -##### TSource - -`TSource` *extends* [`Source`](../type-aliases/Source.md) - -#### Parameters - -##### source - -`TSource` - -An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery - -##### onCallback - -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> - -A function that receives table references and returns the join condition - -#### Returns - -[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`MergeContextWithJoinType`](../type-aliases/MergeContextWithJoinType.md)\<`TContext`, [`SchemaFromSource`](../type-aliases/SchemaFromSource.md)\<`TSource`\>, `"left"`\>\> - -A QueryBuilder with the left joined table available - -#### Example - -```ts -// Left join users with posts -query - .from({ users: usersCollection }) - .leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) + .from({ u: usersCollection }) + .join(joinOf(type, { p: postsCollection }, ({u, p}) => eq(u.id, p.userId))) ``` *** @@ -638,7 +477,7 @@ query limit(count): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:818](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L818) +Defined in: [packages/db/src/query/builder/index.ts:734](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L734) Limit the number of rows returned by the query `orderBy` is required for `limit` @@ -675,7 +514,7 @@ query offset(count): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:842](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L842) +Defined in: [packages/db/src/query/builder/index.ts:758](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L758) Skip a number of rows before returning results `orderBy` is required for `offset` @@ -713,7 +552,7 @@ query orderBy(callback, options): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:697](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L697) +Defined in: [packages/db/src/query/builder/index.ts:609](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L609) Sort the query results by one or more columns @@ -757,53 +596,6 @@ query *** -### rightJoin() - -```ts -rightJoin(source, onCallback): QueryBuilder, "right">>; -``` - -Defined in: [packages/db/src/query/builder/index.ts:416](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L416) - -Perform a RIGHT JOIN with another table or subquery - -#### Type Parameters - -##### TSource - -`TSource` *extends* [`Source`](../type-aliases/Source.md) - -#### Parameters - -##### source - -`TSource` - -An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery - -##### onCallback - -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> - -A function that receives table references and returns the join condition - -#### Returns - -[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`MergeContextWithJoinType`](../type-aliases/MergeContextWithJoinType.md)\<`TContext`, [`SchemaFromSource`](../type-aliases/SchemaFromSource.md)\<`TSource`\>, `"right"`\>\> - -A QueryBuilder with the right joined table available - -#### Example - -```ts -// Right join users with posts -query - .from({ users: usersCollection }) - .rightJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) -``` - -*** - ### select() #### Call Signature @@ -812,7 +604,7 @@ query select(callback): QueryBuilder>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:631](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L631) +Defined in: [packages/db/src/query/builder/index.ts:543](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L543) Select specific columns or computed values from the query @@ -871,7 +663,7 @@ query select(callback): QueryBuilder>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:636](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L636) +Defined in: [packages/db/src/query/builder/index.ts:548](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L548) Select specific columns or computed values from the query @@ -934,7 +726,7 @@ query unionAll(source): QueryBuilder>; ``` -Defined in: [packages/db/src/query/builder/index.ts:274](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L274) +Defined in: [packages/db/src/query/builder/index.ts:278](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L278) Union multiple independent source streams in one query. @@ -974,7 +766,7 @@ query unionAll(...branches): QueryBuilder>; ``` -Defined in: [packages/db/src/query/builder/index.ts:277](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L277) +Defined in: [packages/db/src/query/builder/index.ts:281](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L281) Union multiple independent source streams in one query. @@ -1014,7 +806,7 @@ query where(callback): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:507](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L507) +Defined in: [packages/db/src/query/builder/index.ts:419](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L419) Filter rows based on a condition diff --git a/docs/reference/classes/CannotCombineEmptyExpressionListError.md b/docs/reference/classes/CannotCombineEmptyExpressionListError.md index 1507d2eac6..651e7d1a02 100644 --- a/docs/reference/classes/CannotCombineEmptyExpressionListError.md +++ b/docs/reference/classes/CannotCombineEmptyExpressionListError.md @@ -3,7 +3,7 @@ id: CannotCombineEmptyExpressionListError title: CannotCombineEmptyExpressionListError --- -Defined in: [packages/db/src/errors.ts:1099](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1099) +Defined in: [packages/db/src/errors.ts:1129](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1129) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1099](https://github.com/TanStack/db/blob new CannotCombineEmptyExpressionListError(): CannotCombineEmptyExpressionListError; ``` -Defined in: [packages/db/src/errors.ts:1100](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1100) +Defined in: [packages/db/src/errors.ts:1130](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1130) #### Returns diff --git a/docs/reference/classes/CollectionInputNotFoundError.md b/docs/reference/classes/CollectionInputNotFoundError.md index a680a77cbf..d5517b4dd4 100644 --- a/docs/reference/classes/CollectionInputNotFoundError.md +++ b/docs/reference/classes/CollectionInputNotFoundError.md @@ -3,7 +3,7 @@ id: CollectionInputNotFoundError title: CollectionInputNotFoundError --- -Defined in: [packages/db/src/errors.ts:766](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L766) +Defined in: [packages/db/src/errors.ts:796](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L796) Error thrown when a collection input stream is not found during query compilation. In self-joins, each alias (e.g., 'employee', 'manager') requires its own input stream. @@ -23,7 +23,7 @@ new CollectionInputNotFoundError( availableKeys?): CollectionInputNotFoundError; ``` -Defined in: [packages/db/src/errors.ts:767](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L767) +Defined in: [packages/db/src/errors.ts:797](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L797) #### Parameters diff --git a/docs/reference/classes/CollectionPreloadAbortedError.md b/docs/reference/classes/CollectionPreloadAbortedError.md index 5578be5873..febba48f26 100644 --- a/docs/reference/classes/CollectionPreloadAbortedError.md +++ b/docs/reference/classes/CollectionPreloadAbortedError.md @@ -3,7 +3,7 @@ id: CollectionPreloadAbortedError title: CollectionPreloadAbortedError --- -Defined in: [packages/db/src/errors.ts:1068](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1068) +Defined in: [packages/db/src/errors.ts:1098](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1098) A collection was cleaned up before its initial preload became ready. @@ -19,7 +19,7 @@ A collection was cleaned up before its initial preload became ready. new CollectionPreloadAbortedError(): CollectionPreloadAbortedError; ``` -Defined in: [packages/db/src/errors.ts:1069](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1069) +Defined in: [packages/db/src/errors.ts:1099](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1099) #### Returns diff --git a/docs/reference/classes/DistinctRequiresSelectError.md b/docs/reference/classes/DistinctRequiresSelectError.md index 42ef8ab241..7bcc5a1c2e 100644 --- a/docs/reference/classes/DistinctRequiresSelectError.md +++ b/docs/reference/classes/DistinctRequiresSelectError.md @@ -3,7 +3,7 @@ id: DistinctRequiresSelectError title: DistinctRequiresSelectError --- -Defined in: [packages/db/src/errors.ts:696](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L696) +Defined in: [packages/db/src/errors.ts:716](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L716) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:696](https://github.com/TanStack/db/blob/ new DistinctRequiresSelectError(): DistinctRequiresSelectError; ``` -Defined in: [packages/db/src/errors.ts:697](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L697) +Defined in: [packages/db/src/errors.ts:717](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L717) #### Returns diff --git a/docs/reference/classes/DuplicateAliasInSubqueryError.md b/docs/reference/classes/DuplicateAliasInSubqueryError.md index e9dd8e6dc2..577f6ad714 100644 --- a/docs/reference/classes/DuplicateAliasInSubqueryError.md +++ b/docs/reference/classes/DuplicateAliasInSubqueryError.md @@ -3,7 +3,7 @@ id: DuplicateAliasInSubqueryError title: DuplicateAliasInSubqueryError --- -Defined in: [packages/db/src/errors.ts:798](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L798) +Defined in: [packages/db/src/errors.ts:828](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L828) Retained for callers that imported this formerly thrown error. @@ -24,7 +24,7 @@ are rejected only within one lexical scope or union branch namespace. new DuplicateAliasInSubqueryError(alias, parentAliases): DuplicateAliasInSubqueryError; ``` -Defined in: [packages/db/src/errors.ts:799](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L799) +Defined in: [packages/db/src/errors.ts:829](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L829) #### Parameters diff --git a/docs/reference/classes/EmptyReferencePathError.md b/docs/reference/classes/EmptyReferencePathError.md index f6a0a9ba0c..525dfc92a5 100644 --- a/docs/reference/classes/EmptyReferencePathError.md +++ b/docs/reference/classes/EmptyReferencePathError.md @@ -3,7 +3,7 @@ id: EmptyReferencePathError title: EmptyReferencePathError --- -Defined in: [packages/db/src/errors.ts:831](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L831) +Defined in: [packages/db/src/errors.ts:861](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L861) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:831](https://github.com/TanStack/db/blob/ new EmptyReferencePathError(): EmptyReferencePathError; ``` -Defined in: [packages/db/src/errors.ts:832](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L832) +Defined in: [packages/db/src/errors.ts:862](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L862) #### Returns diff --git a/docs/reference/classes/FnSelectWithGroupByError.md b/docs/reference/classes/FnSelectWithGroupByError.md index f31d7e520c..f3ff9bca84 100644 --- a/docs/reference/classes/FnSelectWithGroupByError.md +++ b/docs/reference/classes/FnSelectWithGroupByError.md @@ -3,7 +3,7 @@ id: FnSelectWithGroupByError title: FnSelectWithGroupByError --- -Defined in: [packages/db/src/errors.ts:706](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L706) +Defined in: [packages/db/src/errors.ts:726](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L726) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:706](https://github.com/TanStack/db/blob/ new FnSelectWithGroupByError(): FnSelectWithGroupByError; ``` -Defined in: [packages/db/src/errors.ts:707](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L707) +Defined in: [packages/db/src/errors.ts:727](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L727) #### Returns diff --git a/docs/reference/classes/GroupByError.md b/docs/reference/classes/GroupByError.md index dda9264335..5c380151c4 100644 --- a/docs/reference/classes/GroupByError.md +++ b/docs/reference/classes/GroupByError.md @@ -3,7 +3,7 @@ id: GroupByError title: GroupByError --- -Defined in: [packages/db/src/errors.ts:940](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L940) +Defined in: [packages/db/src/errors.ts:970](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L970) ## Extends @@ -24,7 +24,7 @@ Defined in: [packages/db/src/errors.ts:940](https://github.com/TanStack/db/blob/ new GroupByError(message): GroupByError; ``` -Defined in: [packages/db/src/errors.ts:941](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L941) +Defined in: [packages/db/src/errors.ts:971](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L971) #### Parameters diff --git a/docs/reference/classes/GroupingOperatorRequiredError.md b/docs/reference/classes/GroupingOperatorRequiredError.md new file mode 100644 index 0000000000..6a720ceeb5 --- /dev/null +++ b/docs/reference/classes/GroupingOperatorRequiredError.md @@ -0,0 +1,212 @@ +--- +id: GroupingOperatorRequiredError +title: GroupingOperatorRequiredError +--- + +Defined in: [packages/db/src/errors.ts:739](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L739) + +## Extends + +- [`QueryCompilationError`](QueryCompilationError.md) + +## Constructors + +### Constructor + +```ts +new GroupingOperatorRequiredError(): GroupingOperatorRequiredError; +``` + +Defined in: [packages/db/src/errors.ts:740](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L740) + +#### Returns + +`GroupingOperatorRequiredError` + +#### Overrides + +[`QueryCompilationError`](QueryCompilationError.md).[`constructor`](QueryCompilationError.md#constructor) + +## Properties + +### cause? + +```ts +optional cause: unknown; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:26 + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`cause`](QueryCompilationError.md#cause) + +*** + +### message + +```ts +message: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1077 + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`message`](QueryCompilationError.md#message) + +*** + +### name + +```ts +name: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`name`](QueryCompilationError.md#name) + +*** + +### stack? + +```ts +optional stack: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1078 + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`stack`](QueryCompilationError.md#stack) + +*** + +### stackTraceLimit + +```ts +static stackTraceLimit: number; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:67 + +The `Error.stackTraceLimit` property specifies the number of stack frames +collected by a stack trace (whether generated by `new Error().stack` or +`Error.captureStackTrace(obj)`). + +The default value is `10` but may be set to any valid JavaScript number. Changes +will affect any stack trace captured _after_ the value has been changed. + +If set to a non-number value, or set to a negative number, stack traces will +not capture any frames. + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`stackTraceLimit`](QueryCompilationError.md#stacktracelimit) + +## Methods + +### captureStackTrace() + +```ts +static captureStackTrace(targetObject, constructorOpt?): void; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:51 + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +##### targetObject + +`object` + +##### constructorOpt? + +`Function` + +#### Returns + +`void` + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`captureStackTrace`](QueryCompilationError.md#capturestacktrace) + +*** + +### prepareStackTrace() + +```ts +static prepareStackTrace(err, stackTraces): any; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:55 + +#### Parameters + +##### err + +`Error` + +##### stackTraces + +`CallSite`[] + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`prepareStackTrace`](QueryCompilationError.md#preparestacktrace) diff --git a/docs/reference/classes/HavingRequiresGroupByError.md b/docs/reference/classes/HavingRequiresGroupByError.md index e11ac460b9..76fc96eecc 100644 --- a/docs/reference/classes/HavingRequiresGroupByError.md +++ b/docs/reference/classes/HavingRequiresGroupByError.md @@ -3,7 +3,7 @@ id: HavingRequiresGroupByError title: HavingRequiresGroupByError --- -Defined in: [packages/db/src/errors.ts:742](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L742) +Defined in: [packages/db/src/errors.ts:772](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L772) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:742](https://github.com/TanStack/db/blob/ new HavingRequiresGroupByError(): HavingRequiresGroupByError; ``` -Defined in: [packages/db/src/errors.ts:743](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L743) +Defined in: [packages/db/src/errors.ts:773](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L773) #### Returns diff --git a/docs/reference/classes/IncludeOperatorRequiredError.md b/docs/reference/classes/IncludeOperatorRequiredError.md new file mode 100644 index 0000000000..b3aad549de --- /dev/null +++ b/docs/reference/classes/IncludeOperatorRequiredError.md @@ -0,0 +1,218 @@ +--- +id: IncludeOperatorRequiredError +title: IncludeOperatorRequiredError +--- + +Defined in: [packages/db/src/errors.ts:613](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L613) + +## Extends + +- [`QueryBuilderError`](QueryBuilderError.md) + +## Constructors + +### Constructor + +```ts +new IncludeOperatorRequiredError(field): IncludeOperatorRequiredError; +``` + +Defined in: [packages/db/src/errors.ts:614](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L614) + +#### Parameters + +##### field + +`string` + +#### Returns + +`IncludeOperatorRequiredError` + +#### Overrides + +[`QueryBuilderError`](QueryBuilderError.md).[`constructor`](QueryBuilderError.md#constructor) + +## Properties + +### cause? + +```ts +optional cause: unknown; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:26 + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`cause`](QueryBuilderError.md#cause) + +*** + +### message + +```ts +message: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1077 + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`message`](QueryBuilderError.md#message) + +*** + +### name + +```ts +name: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`name`](QueryBuilderError.md#name) + +*** + +### stack? + +```ts +optional stack: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1078 + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`stack`](QueryBuilderError.md#stack) + +*** + +### stackTraceLimit + +```ts +static stackTraceLimit: number; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:67 + +The `Error.stackTraceLimit` property specifies the number of stack frames +collected by a stack trace (whether generated by `new Error().stack` or +`Error.captureStackTrace(obj)`). + +The default value is `10` but may be set to any valid JavaScript number. Changes +will affect any stack trace captured _after_ the value has been changed. + +If set to a non-number value, or set to a negative number, stack traces will +not capture any frames. + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`stackTraceLimit`](QueryBuilderError.md#stacktracelimit) + +## Methods + +### captureStackTrace() + +```ts +static captureStackTrace(targetObject, constructorOpt?): void; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:51 + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +##### targetObject + +`object` + +##### constructorOpt? + +`Function` + +#### Returns + +`void` + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`captureStackTrace`](QueryBuilderError.md#capturestacktrace) + +*** + +### prepareStackTrace() + +```ts +static prepareStackTrace(err, stackTraces): any; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:55 + +#### Parameters + +##### err + +`Error` + +##### stackTraces + +`CallSite`[] + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`prepareStackTrace`](QueryBuilderError.md#preparestacktrace) diff --git a/docs/reference/classes/InvalidJoinCondition.md b/docs/reference/classes/InvalidJoinCondition.md index b04f1d4d35..11e655ea21 100644 --- a/docs/reference/classes/InvalidJoinCondition.md +++ b/docs/reference/classes/InvalidJoinCondition.md @@ -3,7 +3,7 @@ id: InvalidJoinCondition title: InvalidJoinCondition --- -Defined in: [packages/db/src/errors.ts:919](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L919) +Defined in: [packages/db/src/errors.ts:949](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L949) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:919](https://github.com/TanStack/db/blob/ new InvalidJoinCondition(): InvalidJoinCondition; ``` -Defined in: [packages/db/src/errors.ts:920](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L920) +Defined in: [packages/db/src/errors.ts:950](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L950) #### Returns diff --git a/docs/reference/classes/InvalidJoinConditionLeftSourceError.md b/docs/reference/classes/InvalidJoinConditionLeftSourceError.md index 262f678750..2e737587e5 100644 --- a/docs/reference/classes/InvalidJoinConditionLeftSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionLeftSourceError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionLeftSourceError title: InvalidJoinConditionLeftSourceError --- -Defined in: [packages/db/src/errors.ts:899](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L899) +Defined in: [packages/db/src/errors.ts:929](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L929) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:899](https://github.com/TanStack/db/blob/ new InvalidJoinConditionLeftSourceError(sourceAlias): InvalidJoinConditionLeftSourceError; ``` -Defined in: [packages/db/src/errors.ts:900](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L900) +Defined in: [packages/db/src/errors.ts:930](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L930) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionRightSourceError.md b/docs/reference/classes/InvalidJoinConditionRightSourceError.md index 5e5006c01c..c8cd77219a 100644 --- a/docs/reference/classes/InvalidJoinConditionRightSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionRightSourceError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionRightSourceError title: InvalidJoinConditionRightSourceError --- -Defined in: [packages/db/src/errors.ts:909](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L909) +Defined in: [packages/db/src/errors.ts:939](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L939) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:909](https://github.com/TanStack/db/blob/ new InvalidJoinConditionRightSourceError(sourceAlias): InvalidJoinConditionRightSourceError; ``` -Defined in: [packages/db/src/errors.ts:910](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L910) +Defined in: [packages/db/src/errors.ts:940](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L940) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionSameSourceError.md b/docs/reference/classes/InvalidJoinConditionSameSourceError.md index 4c04251592..0cda862de5 100644 --- a/docs/reference/classes/InvalidJoinConditionSameSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionSameSourceError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionSameSourceError title: InvalidJoinConditionSameSourceError --- -Defined in: [packages/db/src/errors.ts:879](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L879) +Defined in: [packages/db/src/errors.ts:909](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L909) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:879](https://github.com/TanStack/db/blob/ new InvalidJoinConditionSameSourceError(sourceAlias): InvalidJoinConditionSameSourceError; ``` -Defined in: [packages/db/src/errors.ts:880](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L880) +Defined in: [packages/db/src/errors.ts:910](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L910) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md b/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md index 0465d5a644..c3a0f8d030 100644 --- a/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md +++ b/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionSourceMismatchError title: InvalidJoinConditionSourceMismatchError --- -Defined in: [packages/db/src/errors.ts:889](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L889) +Defined in: [packages/db/src/errors.ts:919](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L919) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:889](https://github.com/TanStack/db/blob/ new InvalidJoinConditionSourceMismatchError(): InvalidJoinConditionSourceMismatchError; ``` -Defined in: [packages/db/src/errors.ts:890](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L890) +Defined in: [packages/db/src/errors.ts:920](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L920) #### Returns diff --git a/docs/reference/classes/InvalidSourceTypeError.md b/docs/reference/classes/InvalidSourceTypeError.md index 6c672ddaaf..fd06e456bf 100644 --- a/docs/reference/classes/InvalidSourceTypeError.md +++ b/docs/reference/classes/InvalidSourceTypeError.md @@ -3,7 +3,7 @@ id: InvalidSourceTypeError title: InvalidSourceTypeError --- -Defined in: [packages/db/src/errors.ts:616](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L616) +Defined in: [packages/db/src/errors.ts:636](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L636) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:616](https://github.com/TanStack/db/blob/ new InvalidSourceTypeError(context, type): InvalidSourceTypeError; ``` -Defined in: [packages/db/src/errors.ts:617](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L617) +Defined in: [packages/db/src/errors.ts:637](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L637) #### Parameters diff --git a/docs/reference/classes/InvalidStorageDataFormatError.md b/docs/reference/classes/InvalidStorageDataFormatError.md index 3c15655e8e..02cc96b418 100644 --- a/docs/reference/classes/InvalidStorageDataFormatError.md +++ b/docs/reference/classes/InvalidStorageDataFormatError.md @@ -3,7 +3,7 @@ id: InvalidStorageDataFormatError title: InvalidStorageDataFormatError --- -Defined in: [packages/db/src/errors.ts:1023](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1023) +Defined in: [packages/db/src/errors.ts:1053](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1053) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1023](https://github.com/TanStack/db/blob new InvalidStorageDataFormatError(storageKey, key): InvalidStorageDataFormatError; ``` -Defined in: [packages/db/src/errors.ts:1024](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1024) +Defined in: [packages/db/src/errors.ts:1054](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1054) #### Parameters diff --git a/docs/reference/classes/InvalidStorageObjectFormatError.md b/docs/reference/classes/InvalidStorageObjectFormatError.md index adb1573d08..641af94173 100644 --- a/docs/reference/classes/InvalidStorageObjectFormatError.md +++ b/docs/reference/classes/InvalidStorageObjectFormatError.md @@ -3,7 +3,7 @@ id: InvalidStorageObjectFormatError title: InvalidStorageObjectFormatError --- -Defined in: [packages/db/src/errors.ts:1033](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1033) +Defined in: [packages/db/src/errors.ts:1063](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1063) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1033](https://github.com/TanStack/db/blob new InvalidStorageObjectFormatError(storageKey): InvalidStorageObjectFormatError; ``` -Defined in: [packages/db/src/errors.ts:1034](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1034) +Defined in: [packages/db/src/errors.ts:1064](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1064) #### Parameters diff --git a/docs/reference/classes/InvalidWhereExpressionError.md b/docs/reference/classes/InvalidWhereExpressionError.md index 623a5eb4ac..b1e5349e40 100644 --- a/docs/reference/classes/InvalidWhereExpressionError.md +++ b/docs/reference/classes/InvalidWhereExpressionError.md @@ -3,7 +3,7 @@ id: InvalidWhereExpressionError title: InvalidWhereExpressionError --- -Defined in: [packages/db/src/errors.ts:661](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L661) +Defined in: [packages/db/src/errors.ts:681](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L681) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:661](https://github.com/TanStack/db/blob/ new InvalidWhereExpressionError(valueType): InvalidWhereExpressionError; ``` -Defined in: [packages/db/src/errors.ts:662](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L662) +Defined in: [packages/db/src/errors.ts:682](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L682) #### Parameters diff --git a/docs/reference/classes/JoinCollectionNotFoundError.md b/docs/reference/classes/JoinCollectionNotFoundError.md index 413f597c7a..45f63eb577 100644 --- a/docs/reference/classes/JoinCollectionNotFoundError.md +++ b/docs/reference/classes/JoinCollectionNotFoundError.md @@ -3,7 +3,7 @@ id: JoinCollectionNotFoundError title: JoinCollectionNotFoundError --- -Defined in: [packages/db/src/errors.ts:851](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L851) +Defined in: [packages/db/src/errors.ts:881](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L881) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:851](https://github.com/TanStack/db/blob/ new JoinCollectionNotFoundError(collectionId): JoinCollectionNotFoundError; ``` -Defined in: [packages/db/src/errors.ts:852](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L852) +Defined in: [packages/db/src/errors.ts:882](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L882) #### Parameters diff --git a/docs/reference/classes/JoinConditionMustBeEqualityError.md b/docs/reference/classes/JoinConditionMustBeEqualityError.md index a3dd24e4ce..4f87f7ab57 100644 --- a/docs/reference/classes/JoinConditionMustBeEqualityError.md +++ b/docs/reference/classes/JoinConditionMustBeEqualityError.md @@ -3,7 +3,7 @@ id: JoinConditionMustBeEqualityError title: JoinConditionMustBeEqualityError --- -Defined in: [packages/db/src/errors.ts:641](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L641) +Defined in: [packages/db/src/errors.ts:661](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L661) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:641](https://github.com/TanStack/db/blob/ new JoinConditionMustBeEqualityError(): JoinConditionMustBeEqualityError; ``` -Defined in: [packages/db/src/errors.ts:642](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L642) +Defined in: [packages/db/src/errors.ts:662](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L662) #### Returns diff --git a/docs/reference/classes/JoinError.md b/docs/reference/classes/JoinError.md index 930a82c829..67a9ad1c47 100644 --- a/docs/reference/classes/JoinError.md +++ b/docs/reference/classes/JoinError.md @@ -3,7 +3,7 @@ id: JoinError title: JoinError --- -Defined in: [packages/db/src/errors.ts:862](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L862) +Defined in: [packages/db/src/errors.ts:892](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L892) ## Extends @@ -27,7 +27,7 @@ Defined in: [packages/db/src/errors.ts:862](https://github.com/TanStack/db/blob/ new JoinError(message): JoinError; ``` -Defined in: [packages/db/src/errors.ts:863](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L863) +Defined in: [packages/db/src/errors.ts:893](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L893) #### Parameters diff --git a/docs/reference/classes/JoinOperatorRequiredError.md b/docs/reference/classes/JoinOperatorRequiredError.md new file mode 100644 index 0000000000..a87145a8b8 --- /dev/null +++ b/docs/reference/classes/JoinOperatorRequiredError.md @@ -0,0 +1,218 @@ +--- +id: JoinOperatorRequiredError +title: JoinOperatorRequiredError +--- + +Defined in: [packages/db/src/errors.ts:623](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L623) + +## Extends + +- [`QueryBuilderError`](QueryBuilderError.md) + +## Constructors + +### Constructor + +```ts +new JoinOperatorRequiredError(type): JoinOperatorRequiredError; +``` + +Defined in: [packages/db/src/errors.ts:624](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L624) + +#### Parameters + +##### type + +`string` + +#### Returns + +`JoinOperatorRequiredError` + +#### Overrides + +[`QueryBuilderError`](QueryBuilderError.md).[`constructor`](QueryBuilderError.md#constructor) + +## Properties + +### cause? + +```ts +optional cause: unknown; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:26 + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`cause`](QueryBuilderError.md#cause) + +*** + +### message + +```ts +message: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1077 + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`message`](QueryBuilderError.md#message) + +*** + +### name + +```ts +name: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`name`](QueryBuilderError.md#name) + +*** + +### stack? + +```ts +optional stack: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1078 + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`stack`](QueryBuilderError.md#stack) + +*** + +### stackTraceLimit + +```ts +static stackTraceLimit: number; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:67 + +The `Error.stackTraceLimit` property specifies the number of stack frames +collected by a stack trace (whether generated by `new Error().stack` or +`Error.captureStackTrace(obj)`). + +The default value is `10` but may be set to any valid JavaScript number. Changes +will affect any stack trace captured _after_ the value has been changed. + +If set to a non-number value, or set to a negative number, stack traces will +not capture any frames. + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`stackTraceLimit`](QueryBuilderError.md#stacktracelimit) + +## Methods + +### captureStackTrace() + +```ts +static captureStackTrace(targetObject, constructorOpt?): void; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:51 + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +##### targetObject + +`object` + +##### constructorOpt? + +`Function` + +#### Returns + +`void` + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`captureStackTrace`](QueryBuilderError.md#capturestacktrace) + +*** + +### prepareStackTrace() + +```ts +static prepareStackTrace(err, stackTraces): any; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:55 + +#### Parameters + +##### err + +`Error` + +##### stackTraces + +`CallSite`[] + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +[`QueryBuilderError`](QueryBuilderError.md).[`prepareStackTrace`](QueryBuilderError.md#preparestacktrace) diff --git a/docs/reference/classes/LimitOffsetRequireOrderByError.md b/docs/reference/classes/LimitOffsetRequireOrderByError.md index 96678fc0e3..8c51ac8b7b 100644 --- a/docs/reference/classes/LimitOffsetRequireOrderByError.md +++ b/docs/reference/classes/LimitOffsetRequireOrderByError.md @@ -3,7 +3,7 @@ id: LimitOffsetRequireOrderByError title: LimitOffsetRequireOrderByError --- -Defined in: [packages/db/src/errors.ts:752](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L752) +Defined in: [packages/db/src/errors.ts:782](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L782) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:752](https://github.com/TanStack/db/blob/ new LimitOffsetRequireOrderByError(): LimitOffsetRequireOrderByError; ``` -Defined in: [packages/db/src/errors.ts:753](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L753) +Defined in: [packages/db/src/errors.ts:783](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L783) #### Returns diff --git a/docs/reference/classes/LoadSubsetOperationAbortedError.md b/docs/reference/classes/LoadSubsetOperationAbortedError.md index 40258450dc..b65e1a933b 100644 --- a/docs/reference/classes/LoadSubsetOperationAbortedError.md +++ b/docs/reference/classes/LoadSubsetOperationAbortedError.md @@ -3,7 +3,7 @@ id: LoadSubsetOperationAbortedError title: LoadSubsetOperationAbortedError --- -Defined in: [packages/db/src/errors.ts:1080](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1080) +Defined in: [packages/db/src/errors.ts:1110](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1110) A subset operation was canceled before its result became visible. @@ -19,7 +19,7 @@ A subset operation was canceled before its result became visible. new LoadSubsetOperationAbortedError(): LoadSubsetOperationAbortedError; ``` -Defined in: [packages/db/src/errors.ts:1081](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1081) +Defined in: [packages/db/src/errors.ts:1111](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1111) #### Returns diff --git a/docs/reference/classes/LocalStorageCollectionError.md b/docs/reference/classes/LocalStorageCollectionError.md index 6555c32205..3a34169cdf 100644 --- a/docs/reference/classes/LocalStorageCollectionError.md +++ b/docs/reference/classes/LocalStorageCollectionError.md @@ -3,7 +3,7 @@ id: LocalStorageCollectionError title: LocalStorageCollectionError --- -Defined in: [packages/db/src/errors.ts:1006](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1006) +Defined in: [packages/db/src/errors.ts:1036](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1036) ## Extends @@ -23,7 +23,7 @@ Defined in: [packages/db/src/errors.ts:1006](https://github.com/TanStack/db/blob new LocalStorageCollectionError(message): LocalStorageCollectionError; ``` -Defined in: [packages/db/src/errors.ts:1007](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1007) +Defined in: [packages/db/src/errors.ts:1037](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1037) #### Parameters diff --git a/docs/reference/classes/MissingAliasInputsError.md b/docs/reference/classes/MissingAliasInputsError.md index 97a76aa771..80fa5052b6 100644 --- a/docs/reference/classes/MissingAliasInputsError.md +++ b/docs/reference/classes/MissingAliasInputsError.md @@ -3,7 +3,7 @@ id: MissingAliasInputsError title: MissingAliasInputsError --- -Defined in: [packages/db/src/errors.ts:1113](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1113) +Defined in: [packages/db/src/errors.ts:1143](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1143) Internal error when the compiler returns aliases that don't have corresponding input streams. This should never happen since all aliases come from user declarations. @@ -20,7 +20,7 @@ This should never happen since all aliases come from user declarations. new MissingAliasInputsError(missingAliases): MissingAliasInputsError; ``` -Defined in: [packages/db/src/errors.ts:1114](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1114) +Defined in: [packages/db/src/errors.ts:1144](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1144) #### Parameters diff --git a/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md b/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md index 64c0f0494a..3005f35446 100644 --- a/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md +++ b/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md @@ -3,7 +3,7 @@ id: NonAggregateExpressionNotInGroupByError title: NonAggregateExpressionNotInGroupByError --- -Defined in: [packages/db/src/errors.ts:947](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L947) +Defined in: [packages/db/src/errors.ts:977](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L977) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:947](https://github.com/TanStack/db/blob/ new NonAggregateExpressionNotInGroupByError(alias): NonAggregateExpressionNotInGroupByError; ``` -Defined in: [packages/db/src/errors.ts:948](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L948) +Defined in: [packages/db/src/errors.ts:978](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L978) #### Parameters diff --git a/docs/reference/classes/QueryBuilderError.md b/docs/reference/classes/QueryBuilderError.md index dccf34f433..cc05f01b77 100644 --- a/docs/reference/classes/QueryBuilderError.md +++ b/docs/reference/classes/QueryBuilderError.md @@ -14,6 +14,8 @@ Defined in: [packages/db/src/errors.ts:576](https://github.com/TanStack/db/blob/ - [`OnlyOneSourceAllowedError`](OnlyOneSourceAllowedError.md) - [`SubQueryMustHaveFromClauseError`](SubQueryMustHaveFromClauseError.md) - [`InvalidSourceError`](InvalidSourceError.md) +- [`IncludeOperatorRequiredError`](IncludeOperatorRequiredError.md) +- [`JoinOperatorRequiredError`](JoinOperatorRequiredError.md) - [`InvalidSourceTypeError`](InvalidSourceTypeError.md) - [`JoinConditionMustBeEqualityError`](JoinConditionMustBeEqualityError.md) - [`QueryMustHaveFromClauseError`](QueryMustHaveFromClauseError.md) diff --git a/docs/reference/classes/QueryCompilationError.md b/docs/reference/classes/QueryCompilationError.md index 081cd0086b..efc8fbd06a 100644 --- a/docs/reference/classes/QueryCompilationError.md +++ b/docs/reference/classes/QueryCompilationError.md @@ -3,7 +3,7 @@ id: QueryCompilationError title: QueryCompilationError --- -Defined in: [packages/db/src/errors.ts:677](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L677) +Defined in: [packages/db/src/errors.ts:697](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L697) ## Extends @@ -14,6 +14,7 @@ Defined in: [packages/db/src/errors.ts:677](https://github.com/TanStack/db/blob/ - [`UnsafeAliasPathError`](UnsafeAliasPathError.md) - [`DistinctRequiresSelectError`](DistinctRequiresSelectError.md) - [`FnSelectWithGroupByError`](FnSelectWithGroupByError.md) +- [`GroupingOperatorRequiredError`](GroupingOperatorRequiredError.md) - [`UnsupportedFnSelectResultError`](UnsupportedFnSelectResultError.md) - [`UnsupportedRootScalarSelectError`](UnsupportedRootScalarSelectError.md) - [`HavingRequiresGroupByError`](HavingRequiresGroupByError.md) @@ -37,7 +38,7 @@ Defined in: [packages/db/src/errors.ts:677](https://github.com/TanStack/db/blob/ new QueryCompilationError(message): QueryCompilationError; ``` -Defined in: [packages/db/src/errors.ts:678](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L678) +Defined in: [packages/db/src/errors.ts:698](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L698) #### Parameters diff --git a/docs/reference/classes/QueryMustHaveFromClauseError.md b/docs/reference/classes/QueryMustHaveFromClauseError.md index efffac467e..41f5655047 100644 --- a/docs/reference/classes/QueryMustHaveFromClauseError.md +++ b/docs/reference/classes/QueryMustHaveFromClauseError.md @@ -3,7 +3,7 @@ id: QueryMustHaveFromClauseError title: QueryMustHaveFromClauseError --- -Defined in: [packages/db/src/errors.ts:651](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L651) +Defined in: [packages/db/src/errors.ts:671](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L671) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:651](https://github.com/TanStack/db/blob/ new QueryMustHaveFromClauseError(): QueryMustHaveFromClauseError; ``` -Defined in: [packages/db/src/errors.ts:652](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L652) +Defined in: [packages/db/src/errors.ts:672](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L672) #### Returns diff --git a/docs/reference/classes/QueryOptimizerError.md b/docs/reference/classes/QueryOptimizerError.md index 9951d5cb43..a698c592ce 100644 --- a/docs/reference/classes/QueryOptimizerError.md +++ b/docs/reference/classes/QueryOptimizerError.md @@ -3,7 +3,7 @@ id: QueryOptimizerError title: QueryOptimizerError --- -Defined in: [packages/db/src/errors.ts:1092](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1092) +Defined in: [packages/db/src/errors.ts:1122](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1122) ## Extends @@ -21,7 +21,7 @@ Defined in: [packages/db/src/errors.ts:1092](https://github.com/TanStack/db/blob new QueryOptimizerError(message): QueryOptimizerError; ``` -Defined in: [packages/db/src/errors.ts:1093](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1093) +Defined in: [packages/db/src/errors.ts:1123](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1123) #### Parameters diff --git a/docs/reference/classes/SerializationError.md b/docs/reference/classes/SerializationError.md index 062bcd34b1..ec30df6fbe 100644 --- a/docs/reference/classes/SerializationError.md +++ b/docs/reference/classes/SerializationError.md @@ -3,7 +3,7 @@ id: SerializationError title: SerializationError --- -Defined in: [packages/db/src/errors.ts:995](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L995) +Defined in: [packages/db/src/errors.ts:1025](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1025) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:995](https://github.com/TanStack/db/blob/ new SerializationError(operation, originalError): SerializationError; ``` -Defined in: [packages/db/src/errors.ts:996](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L996) +Defined in: [packages/db/src/errors.ts:1026](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1026) #### Parameters diff --git a/docs/reference/classes/SetWindowFindOneLimitError.md b/docs/reference/classes/SetWindowFindOneLimitError.md index a060541d64..4eb3180e58 100644 --- a/docs/reference/classes/SetWindowFindOneLimitError.md +++ b/docs/reference/classes/SetWindowFindOneLimitError.md @@ -3,7 +3,7 @@ id: SetWindowFindOneLimitError title: SetWindowFindOneLimitError --- -Defined in: [packages/db/src/errors.ts:1141](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1141) +Defined in: [packages/db/src/errors.ts:1171](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1171) Error thrown when setWindow would change the limit of a findOne() query. @@ -19,7 +19,7 @@ Error thrown when setWindow would change the limit of a findOne() query. new SetWindowFindOneLimitError(): SetWindowFindOneLimitError; ``` -Defined in: [packages/db/src/errors.ts:1142](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1142) +Defined in: [packages/db/src/errors.ts:1172](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1172) #### Returns diff --git a/docs/reference/classes/SetWindowReentrancyError.md b/docs/reference/classes/SetWindowReentrancyError.md index f8a7b0285a..863fd852f6 100644 --- a/docs/reference/classes/SetWindowReentrancyError.md +++ b/docs/reference/classes/SetWindowReentrancyError.md @@ -3,7 +3,7 @@ id: SetWindowReentrancyError title: SetWindowReentrancyError --- -Defined in: [packages/db/src/errors.ts:1152](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1152) +Defined in: [packages/db/src/errors.ts:1182](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1182) Error thrown when setWindow is called from inside another setWindow call. @@ -19,7 +19,7 @@ Error thrown when setWindow is called from inside another setWindow call. new SetWindowReentrancyError(): SetWindowReentrancyError; ``` -Defined in: [packages/db/src/errors.ts:1153](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1153) +Defined in: [packages/db/src/errors.ts:1183](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1183) #### Returns diff --git a/docs/reference/classes/SetWindowRequiresOrderByError.md b/docs/reference/classes/SetWindowRequiresOrderByError.md index 9ce0624498..1cfa34eedd 100644 --- a/docs/reference/classes/SetWindowRequiresOrderByError.md +++ b/docs/reference/classes/SetWindowRequiresOrderByError.md @@ -3,7 +3,7 @@ id: SetWindowRequiresOrderByError title: SetWindowRequiresOrderByError --- -Defined in: [packages/db/src/errors.ts:1127](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1127) +Defined in: [packages/db/src/errors.ts:1157](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1157) Error thrown when setWindow is called on a collection without an ORDER BY clause. @@ -19,7 +19,7 @@ Error thrown when setWindow is called on a collection without an ORDER BY clause new SetWindowRequiresOrderByError(): SetWindowRequiresOrderByError; ``` -Defined in: [packages/db/src/errors.ts:1128](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1128) +Defined in: [packages/db/src/errors.ts:1158](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1158) #### Returns diff --git a/docs/reference/classes/StorageError.md b/docs/reference/classes/StorageError.md index 57ea54b824..b27a79ecb2 100644 --- a/docs/reference/classes/StorageError.md +++ b/docs/reference/classes/StorageError.md @@ -3,7 +3,7 @@ id: StorageError title: StorageError --- -Defined in: [packages/db/src/errors.ts:988](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L988) +Defined in: [packages/db/src/errors.ts:1018](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1018) ## Extends @@ -22,7 +22,7 @@ Defined in: [packages/db/src/errors.ts:988](https://github.com/TanStack/db/blob/ new StorageError(message): StorageError; ``` -Defined in: [packages/db/src/errors.ts:989](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L989) +Defined in: [packages/db/src/errors.ts:1019](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1019) #### Parameters diff --git a/docs/reference/classes/StorageKeyRequiredError.md b/docs/reference/classes/StorageKeyRequiredError.md index d5409d93f0..8a117370f2 100644 --- a/docs/reference/classes/StorageKeyRequiredError.md +++ b/docs/reference/classes/StorageKeyRequiredError.md @@ -3,7 +3,7 @@ id: StorageKeyRequiredError title: StorageKeyRequiredError --- -Defined in: [packages/db/src/errors.ts:1013](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1013) +Defined in: [packages/db/src/errors.ts:1043](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1043) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1013](https://github.com/TanStack/db/blob new StorageKeyRequiredError(): StorageKeyRequiredError; ``` -Defined in: [packages/db/src/errors.ts:1014](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1014) +Defined in: [packages/db/src/errors.ts:1044](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1044) #### Returns diff --git a/docs/reference/classes/SyncCleanupError.md b/docs/reference/classes/SyncCleanupError.md index 7cb34e4aef..efaa54d67f 100644 --- a/docs/reference/classes/SyncCleanupError.md +++ b/docs/reference/classes/SyncCleanupError.md @@ -3,7 +3,7 @@ id: SyncCleanupError title: SyncCleanupError --- -Defined in: [packages/db/src/errors.ts:1044](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1044) +Defined in: [packages/db/src/errors.ts:1074](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1074) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1044](https://github.com/TanStack/db/blob new SyncCleanupError(collectionId, error): SyncCleanupError; ``` -Defined in: [packages/db/src/errors.ts:1045](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1045) +Defined in: [packages/db/src/errors.ts:1075](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1075) #### Parameters diff --git a/docs/reference/classes/SyncTransactionAbortedError.md b/docs/reference/classes/SyncTransactionAbortedError.md index 6a05e786f9..77815f83e3 100644 --- a/docs/reference/classes/SyncTransactionAbortedError.md +++ b/docs/reference/classes/SyncTransactionAbortedError.md @@ -3,7 +3,7 @@ id: SyncTransactionAbortedError title: SyncTransactionAbortedError --- -Defined in: [packages/db/src/errors.ts:1056](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1056) +Defined in: [packages/db/src/errors.ts:1086](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1086) A sync transaction was canceled before its writes became visible. @@ -19,7 +19,7 @@ A sync transaction was canceled before its writes became visible. new SyncTransactionAbortedError(): SyncTransactionAbortedError; ``` -Defined in: [packages/db/src/errors.ts:1057](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1057) +Defined in: [packages/db/src/errors.ts:1087](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1087) #### Returns diff --git a/docs/reference/classes/UnhashableQueryIRError.md b/docs/reference/classes/UnhashableQueryIRError.md index e6ab2621e5..a6a4958970 100644 --- a/docs/reference/classes/UnhashableQueryIRError.md +++ b/docs/reference/classes/UnhashableQueryIRError.md @@ -3,7 +3,7 @@ id: UnhashableQueryIRError title: UnhashableQueryIRError --- -Defined in: [packages/db/src/errors.ts:1163](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1163) +Defined in: [packages/db/src/errors.ts:1193](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1193) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1163](https://github.com/TanStack/db/blob new UnhashableQueryIRError(path, reason): UnhashableQueryIRError; ``` -Defined in: [packages/db/src/errors.ts:1164](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1164) +Defined in: [packages/db/src/errors.ts:1194](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1194) #### Parameters @@ -95,7 +95,7 @@ Error.name readonly path: string; ``` -Defined in: [packages/db/src/errors.ts:1165](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1165) +Defined in: [packages/db/src/errors.ts:1195](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1195) *** @@ -105,7 +105,7 @@ Defined in: [packages/db/src/errors.ts:1165](https://github.com/TanStack/db/blob readonly reason: string; ``` -Defined in: [packages/db/src/errors.ts:1166](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1166) +Defined in: [packages/db/src/errors.ts:1196](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1196) *** diff --git a/docs/reference/classes/UnknownExpressionTypeError.md b/docs/reference/classes/UnknownExpressionTypeError.md index 5ec84e8123..520f60adf3 100644 --- a/docs/reference/classes/UnknownExpressionTypeError.md +++ b/docs/reference/classes/UnknownExpressionTypeError.md @@ -3,7 +3,7 @@ id: UnknownExpressionTypeError title: UnknownExpressionTypeError --- -Defined in: [packages/db/src/errors.ts:821](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L821) +Defined in: [packages/db/src/errors.ts:851](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L851) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:821](https://github.com/TanStack/db/blob/ new UnknownExpressionTypeError(type): UnknownExpressionTypeError; ``` -Defined in: [packages/db/src/errors.ts:822](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L822) +Defined in: [packages/db/src/errors.ts:852](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L852) #### Parameters diff --git a/docs/reference/classes/UnknownFunctionError.md b/docs/reference/classes/UnknownFunctionError.md index eb91c7b91a..5e80b672c1 100644 --- a/docs/reference/classes/UnknownFunctionError.md +++ b/docs/reference/classes/UnknownFunctionError.md @@ -3,7 +3,7 @@ id: UnknownFunctionError title: UnknownFunctionError --- -Defined in: [packages/db/src/errors.ts:841](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L841) +Defined in: [packages/db/src/errors.ts:871](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L871) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:841](https://github.com/TanStack/db/blob/ new UnknownFunctionError(functionName): UnknownFunctionError; ``` -Defined in: [packages/db/src/errors.ts:842](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L842) +Defined in: [packages/db/src/errors.ts:872](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L872) #### Parameters diff --git a/docs/reference/classes/UnknownHavingExpressionTypeError.md b/docs/reference/classes/UnknownHavingExpressionTypeError.md index d8ab35a3e9..9b4e05f66b 100644 --- a/docs/reference/classes/UnknownHavingExpressionTypeError.md +++ b/docs/reference/classes/UnknownHavingExpressionTypeError.md @@ -3,7 +3,7 @@ id: UnknownHavingExpressionTypeError title: UnknownHavingExpressionTypeError --- -Defined in: [packages/db/src/errors.ts:977](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L977) +Defined in: [packages/db/src/errors.ts:1007](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1007) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:977](https://github.com/TanStack/db/blob/ new UnknownHavingExpressionTypeError(type): UnknownHavingExpressionTypeError; ``` -Defined in: [packages/db/src/errors.ts:978](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L978) +Defined in: [packages/db/src/errors.ts:1008](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1008) #### Parameters diff --git a/docs/reference/classes/UnsafeAliasPathError.md b/docs/reference/classes/UnsafeAliasPathError.md index ce1e02fcdb..51f590e15f 100644 --- a/docs/reference/classes/UnsafeAliasPathError.md +++ b/docs/reference/classes/UnsafeAliasPathError.md @@ -3,7 +3,7 @@ id: UnsafeAliasPathError title: UnsafeAliasPathError --- -Defined in: [packages/db/src/errors.ts:684](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L684) +Defined in: [packages/db/src/errors.ts:704](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L704) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:684](https://github.com/TanStack/db/blob/ new UnsafeAliasPathError(segment): UnsafeAliasPathError; ``` -Defined in: [packages/db/src/errors.ts:685](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L685) +Defined in: [packages/db/src/errors.ts:705](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L705) #### Parameters diff --git a/docs/reference/classes/UnsupportedAggregateFunctionError.md b/docs/reference/classes/UnsupportedAggregateFunctionError.md index 8173b0e2e5..d5b1dfdf0f 100644 --- a/docs/reference/classes/UnsupportedAggregateFunctionError.md +++ b/docs/reference/classes/UnsupportedAggregateFunctionError.md @@ -3,7 +3,7 @@ id: UnsupportedAggregateFunctionError title: UnsupportedAggregateFunctionError --- -Defined in: [packages/db/src/errors.ts:957](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L957) +Defined in: [packages/db/src/errors.ts:987](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L987) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:957](https://github.com/TanStack/db/blob/ new UnsupportedAggregateFunctionError(functionName): UnsupportedAggregateFunctionError; ``` -Defined in: [packages/db/src/errors.ts:958](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L958) +Defined in: [packages/db/src/errors.ts:988](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L988) #### Parameters diff --git a/docs/reference/classes/UnsupportedFnSelectResultError.md b/docs/reference/classes/UnsupportedFnSelectResultError.md index 5b79a8108d..341266aed3 100644 --- a/docs/reference/classes/UnsupportedFnSelectResultError.md +++ b/docs/reference/classes/UnsupportedFnSelectResultError.md @@ -3,7 +3,7 @@ id: UnsupportedFnSelectResultError title: UnsupportedFnSelectResultError --- -Defined in: [packages/db/src/errors.ts:719](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L719) +Defined in: [packages/db/src/errors.ts:749](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L749) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:719](https://github.com/TanStack/db/blob/ new UnsupportedFnSelectResultError(valueDescription): UnsupportedFnSelectResultError; ``` -Defined in: [packages/db/src/errors.ts:720](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L720) +Defined in: [packages/db/src/errors.ts:750](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L750) #### Parameters diff --git a/docs/reference/classes/UnsupportedFromTypeError.md b/docs/reference/classes/UnsupportedFromTypeError.md index 2bb3716416..0df5173fd1 100644 --- a/docs/reference/classes/UnsupportedFromTypeError.md +++ b/docs/reference/classes/UnsupportedFromTypeError.md @@ -3,7 +3,7 @@ id: UnsupportedFromTypeError title: UnsupportedFromTypeError --- -Defined in: [packages/db/src/errors.ts:811](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L811) +Defined in: [packages/db/src/errors.ts:841](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L841) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:811](https://github.com/TanStack/db/blob/ new UnsupportedFromTypeError(type): UnsupportedFromTypeError; ``` -Defined in: [packages/db/src/errors.ts:812](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L812) +Defined in: [packages/db/src/errors.ts:842](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L842) #### Parameters diff --git a/docs/reference/classes/UnsupportedJoinSourceTypeError.md b/docs/reference/classes/UnsupportedJoinSourceTypeError.md index f2352e4f8b..ce7248b61f 100644 --- a/docs/reference/classes/UnsupportedJoinSourceTypeError.md +++ b/docs/reference/classes/UnsupportedJoinSourceTypeError.md @@ -3,7 +3,7 @@ id: UnsupportedJoinSourceTypeError title: UnsupportedJoinSourceTypeError --- -Defined in: [packages/db/src/errors.ts:929](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L929) +Defined in: [packages/db/src/errors.ts:959](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L959) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:929](https://github.com/TanStack/db/blob/ new UnsupportedJoinSourceTypeError(type): UnsupportedJoinSourceTypeError; ``` -Defined in: [packages/db/src/errors.ts:930](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L930) +Defined in: [packages/db/src/errors.ts:960](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L960) #### Parameters diff --git a/docs/reference/classes/UnsupportedJoinTypeError.md b/docs/reference/classes/UnsupportedJoinTypeError.md index fd08b7ef5d..5215a14c85 100644 --- a/docs/reference/classes/UnsupportedJoinTypeError.md +++ b/docs/reference/classes/UnsupportedJoinTypeError.md @@ -3,7 +3,7 @@ id: UnsupportedJoinTypeError title: UnsupportedJoinTypeError --- -Defined in: [packages/db/src/errors.ts:869](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L869) +Defined in: [packages/db/src/errors.ts:899](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L899) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:869](https://github.com/TanStack/db/blob/ new UnsupportedJoinTypeError(joinType): UnsupportedJoinTypeError; ``` -Defined in: [packages/db/src/errors.ts:870](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L870) +Defined in: [packages/db/src/errors.ts:900](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L900) #### Parameters diff --git a/docs/reference/classes/UnsupportedRootScalarSelectError.md b/docs/reference/classes/UnsupportedRootScalarSelectError.md index 02d20a9454..d1f378beab 100644 --- a/docs/reference/classes/UnsupportedRootScalarSelectError.md +++ b/docs/reference/classes/UnsupportedRootScalarSelectError.md @@ -3,7 +3,7 @@ id: UnsupportedRootScalarSelectError title: UnsupportedRootScalarSelectError --- -Defined in: [packages/db/src/errors.ts:731](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L731) +Defined in: [packages/db/src/errors.ts:761](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L761) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:731](https://github.com/TanStack/db/blob/ new UnsupportedRootScalarSelectError(): UnsupportedRootScalarSelectError; ``` -Defined in: [packages/db/src/errors.ts:732](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L732) +Defined in: [packages/db/src/errors.ts:762](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L762) #### Returns diff --git a/docs/reference/functions/compileExpression.md b/docs/reference/functions/compileExpression.md index fb7e934830..86f62d4310 100644 --- a/docs/reference/functions/compileExpression.md +++ b/docs/reference/functions/compileExpression.md @@ -4,7 +4,7 @@ title: compileExpression --- ```ts -function compileExpression(expr, isSingleRow): CompiledSingleRowExpression | CompiledExpression; +function compileExpression(expr, isSingleRow): CompiledExpression | CompiledSingleRowExpression; ``` Defined in: [packages/db/src/query/compiler/evaluators.ts:103](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/evaluators.ts#L103) @@ -24,4 +24,4 @@ This eliminates branching during evaluation by pre-compiling the expression stru ## Returns -`CompiledSingleRowExpression` \| `CompiledExpression` +`CompiledExpression` \| `CompiledSingleRowExpression` diff --git a/docs/reference/functions/compileQuery.md b/docs/reference/functions/compileQuery.md index 4e0bb08a2a..06f9df8351 100644 --- a/docs/reference/functions/compileQuery.md +++ b/docs/reference/functions/compileQuery.md @@ -7,7 +7,7 @@ title: compileQuery function compileQuery(...args): CompilationResult; ``` -Defined in: [packages/db/src/query/compiler/index.ts:405](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/index.ts#L405) +Defined in: [packages/db/src/query/compiler/index.ts:409](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/index.ts#L409) ## Parameters diff --git a/docs/reference/functions/createLiveQueryObserver.md b/docs/reference/functions/createLiveQueryObserver.md index 5dd16de942..d7d96b0dc2 100644 --- a/docs/reference/functions/createLiveQueryObserver.md +++ b/docs/reference/functions/createLiveQueryObserver.md @@ -7,7 +7,7 @@ title: createLiveQueryObserver function createLiveQueryObserver(collection, options): LiveQueryObserver; ``` -Defined in: [packages/db/src/live-query-observer.ts:1203](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1203) +Defined in: [packages/db/src/live-query-observer.ts:1131](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1131) **`Internal`** diff --git a/docs/reference/functions/getPersistedReadinessSource.md b/docs/reference/functions/getPersistedReadinessSource.md index 2cf3ededbe..2901fd91a4 100644 --- a/docs/reference/functions/getPersistedReadinessSource.md +++ b/docs/reference/functions/getPersistedReadinessSource.md @@ -9,7 +9,7 @@ function getPersistedReadinessSource(config): | undefined; ``` -Defined in: [packages/db/src/persisted-readiness.ts:20](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L20) +Defined in: [packages/db/src/persisted-readiness.ts:160](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L160) ## Parameters diff --git a/docs/reference/functions/getPreparedLiveQueryIdentity.md b/docs/reference/functions/getPreparedLiveQueryIdentity.md index ed0c5a8b18..21a2960981 100644 --- a/docs/reference/functions/getPreparedLiveQueryIdentity.md +++ b/docs/reference/functions/getPreparedLiveQueryIdentity.md @@ -4,10 +4,10 @@ title: getPreparedLiveQueryIdentity --- ```ts -function getPreparedLiveQueryIdentity(value): unknown; +function getPreparedLiveQueryIdentity(value, pool?): unknown; ``` -Defined in: [packages/db/src/live-query-options.ts:165](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L165) +Defined in: [packages/db/src/live-query-options.ts:162](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L162) ## Parameters @@ -15,6 +15,18 @@ Defined in: [packages/db/src/live-query-options.ts:165](https://github.com/TanSt `unknown` +### pool? + +#### createPooledLiveQuery + +(`query`, `__namedParameters`) => + \| [`Collection`](../interfaces/Collection.md)\<`any`, `any`, `any`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `any`\> + \| `undefined` + +#### getPooledQueryIdentity + +(`query`) => `string` \| `undefined` + ## Returns `unknown` diff --git a/docs/reference/functions/getPreparedLiveQuerySources.md b/docs/reference/functions/getPreparedLiveQuerySources.md index 9752ca50cd..74890ee9f9 100644 --- a/docs/reference/functions/getPreparedLiveQuerySources.md +++ b/docs/reference/functions/getPreparedLiveQuerySources.md @@ -7,7 +7,7 @@ title: getPreparedLiveQuerySources function getPreparedLiveQuerySources(value, onUnboundDescriptor?): Collection, string | number, UtilsRecord, StandardSchemaV1, Record>[]; ``` -Defined in: [packages/db/src/live-query-options.ts:142](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L142) +Defined in: [packages/db/src/live-query-options.ts:139](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L139) Concrete source objects in query-position order for a prepared value. diff --git a/docs/reference/functions/groupKeys.md b/docs/reference/functions/groupKeys.md new file mode 100644 index 0000000000..35e40986a1 --- /dev/null +++ b/docs/reference/functions/groupKeys.md @@ -0,0 +1,26 @@ +--- +id: groupKeys +title: groupKeys +--- + +```ts +function groupKeys(callback): GroupKeys; +``` + +Defined in: [packages/db/src/query/builder/group-keys.ts:14](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/group-keys.ts#L14) + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) + +## Parameters + +### callback + +[`GroupByCallback`](../type-aliases/GroupByCallback.md)\<`TContext`\> + +## Returns + +[`GroupKeys`](../interfaces/GroupKeys.md)\<`TContext`\> diff --git a/docs/reference/functions/include.md b/docs/reference/functions/include.md new file mode 100644 index 0000000000..aa79e84936 --- /dev/null +++ b/docs/reference/functions/include.md @@ -0,0 +1,40 @@ +--- +id: include +title: include +--- + +```ts +function include(query): IncludeWrapper; +``` + +Defined in: [packages/db/src/query/builder/include-functions.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/include-functions.ts#L74) + +Includes a correlated subquery as a live child Collection on each parent +row. Use `toArray()` or `materialize()` for inline values instead. + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) + +## Parameters + +### query + +[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<`TContext`\> + +## Returns + +`IncludeWrapper`\<`TContext`\> + +## Example + +```ts +select(({ p }) => ({ + ...p, + issues: include( + q.from({ i: issues }).where(({ i }) => eq(i.projectId, p.id)), + ), +})) +``` diff --git a/docs/reference/functions/joinOf.md b/docs/reference/functions/joinOf.md new file mode 100644 index 0000000000..129a4d2153 --- /dev/null +++ b/docs/reference/functions/joinOf.md @@ -0,0 +1,47 @@ +--- +id: joinOf +title: joinOf +--- + +```ts +function joinOf( + type, + source, +on): JoinOperator; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L58) + +A join whose type is chosen at run time. + +## Type Parameters + +### TJoinType + +`TJoinType` *extends* [`JoinType`](../type-aliases/JoinType.md) + +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) + +### TSource + +`TSource` *extends* [`Source`](../type-aliases/Source.md) + +## Parameters + +### type + +`TJoinType` + +### source + +`TSource` + +### on + +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> + +## Returns + +[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `TJoinType`\> diff --git a/docs/reference/functions/materialize.md b/docs/reference/functions/materialize.md index 0bcde08a7d..d505420a4f 100644 --- a/docs/reference/functions/materialize.md +++ b/docs/reference/functions/materialize.md @@ -7,7 +7,7 @@ title: materialize function materialize(query): MaterializeWrapper, TContext extends SingleResult ? true : false>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:882](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L882) +Defined in: [packages/db/src/query/builder/include-functions.ts:51](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/include-functions.ts#L51) Materialize an includes subquery into a plain value on the parent row. diff --git a/docs/reference/functions/prepareLiveQueryValue.md b/docs/reference/functions/prepareLiveQueryValue.md index 8a7c287e3c..9fc6718bf1 100644 --- a/docs/reference/functions/prepareLiveQueryValue.md +++ b/docs/reference/functions/prepareLiveQueryValue.md @@ -10,7 +10,7 @@ function prepareLiveQueryValue( deferredCollections?): unknown; ``` -Defined in: [packages/db/src/live-query-options.ts:89](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L89) +Defined in: [packages/db/src/live-query-options.ts:86](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L86) ## Parameters diff --git a/docs/reference/functions/resolveLiveQueryValue.md b/docs/reference/functions/resolveLiveQueryValue.md index 47c59c06b4..1f4f3d0b50 100644 --- a/docs/reference/functions/resolveLiveQueryValue.md +++ b/docs/reference/functions/resolveLiveQueryValue.md @@ -9,7 +9,7 @@ function resolveLiveQueryValue(value, __namedParameters): | null; ``` -Defined in: [packages/db/src/live-query-options.ts:229](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L229) +Defined in: [packages/db/src/live-query-options.ts:232](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L232) ## Parameters @@ -25,7 +25,13 @@ Defined in: [packages/db/src/live-query-options.ts:229](https://github.com/TanSt #### pool? -`boolean` = `true` + \| `false` + \| \{ + `createPooledLiveQuery`: (`query`, `__namedParameters`) => + \| [`Collection`](../interfaces/Collection.md)\<`any`, `any`, `any`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `any`\> + \| `undefined`; + `getPooledQueryIdentity`: (`query`) => `string` \| `undefined`; +\} #### startSync? diff --git a/docs/reference/functions/resumeDeferredLiveQueryCollections.md b/docs/reference/functions/resumeDeferredLiveQueryCollections.md index f42da0e2ef..6a945d103d 100644 --- a/docs/reference/functions/resumeDeferredLiveQueryCollections.md +++ b/docs/reference/functions/resumeDeferredLiveQueryCollections.md @@ -7,7 +7,7 @@ title: resumeDeferredLiveQueryCollections function resumeDeferredLiveQueryCollections(collections): void; ``` -Defined in: [packages/db/src/live-query-options.ts:36](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L36) +Defined in: [packages/db/src/live-query-options.ts:33](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L33) Release every deferred source, then report the first startup failure. diff --git a/docs/reference/functions/toArray.md b/docs/reference/functions/toArray.md index 009f0d310d..c1e08d6d18 100644 --- a/docs/reference/functions/toArray.md +++ b/docs/reference/functions/toArray.md @@ -7,7 +7,7 @@ title: toArray function toArray(query): ToArrayWrapper>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:805](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L805) +Defined in: [packages/db/src/query/builder/include-functions.ts:15](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/include-functions.ts#L15) ## Type Parameters diff --git a/docs/reference/functions/waitForPersistedInitialRender.md b/docs/reference/functions/waitForPersistedInitialRender.md new file mode 100644 index 0000000000..009df2518c --- /dev/null +++ b/docs/reference/functions/waitForPersistedInitialRender.md @@ -0,0 +1,40 @@ +--- +id: waitForPersistedInitialRender +title: waitForPersistedInitialRender +--- + +```ts +function waitForPersistedInitialRender(__namedParameters): object; +``` + +Defined in: [packages/db/src/persisted-readiness.ts:56](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L56) + +Resolve when network data is ready, or when the network deadline passes or +the network fails while every persisted source has restored. Reject with +the network error when no persisted fallback may render. + +## Parameters + +### \_\_namedParameters + +[`PersistedInitialRenderInput`](../interfaces/PersistedInitialRenderInput.md) + +## Returns + +`object` + +### cancel() + +```ts +cancel: () => void; +``` + +#### Returns + +`void` + +### promise + +```ts +promise: Promise; +``` diff --git a/docs/reference/index.md b/docs/reference/index.md index 486890d811..23f79b3596 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -40,7 +40,9 @@ title: "@tanstack/db" - [FnSelectWithGroupByError](classes/FnSelectWithGroupByError.md) - [GetKeyRequiredError](classes/GetKeyRequiredError.md) - [GroupByError](classes/GroupByError.md) +- [GroupingOperatorRequiredError](classes/GroupingOperatorRequiredError.md) - [HavingRequiresGroupByError](classes/HavingRequiresGroupByError.md) +- [IncludeOperatorRequiredError](classes/IncludeOperatorRequiredError.md) - [InvalidCollectionStatusTransitionError](classes/InvalidCollectionStatusTransitionError.md) - [InvalidJoinCondition](classes/InvalidJoinCondition.md) - [InvalidJoinConditionLeftSourceError](classes/InvalidJoinConditionLeftSourceError.md) @@ -58,6 +60,7 @@ title: "@tanstack/db" - [JoinCollectionNotFoundError](classes/JoinCollectionNotFoundError.md) - [JoinConditionMustBeEqualityError](classes/JoinConditionMustBeEqualityError.md) - [JoinError](classes/JoinError.md) +- [JoinOperatorRequiredError](classes/JoinOperatorRequiredError.md) - [KeyUpdateNotAllowedError](classes/KeyUpdateNotAllowedError.md) - [LimitOffsetRequireOrderByError](classes/LimitOffsetRequireOrderByError.md) - [LiveQueryObserverDisposedError](classes/LiveQueryObserverDisposedError.md) @@ -148,6 +151,7 @@ title: "@tanstack/db" - [Effect](interfaces/Effect.md) - [EffectConfig](interfaces/EffectConfig.md) - [EffectContext](interfaces/EffectContext.md) +- [GroupKeys](interfaces/GroupKeys.md) - [IndexDevModeConfig](interfaces/IndexDevModeConfig.md) - [IndexedDBCollectionConfig](interfaces/IndexedDBCollectionConfig.md) - [IndexedDBCollectionUtils](interfaces/IndexedDBCollectionUtils.md) @@ -156,6 +160,7 @@ title: "@tanstack/db" - [IndexOptions](interfaces/IndexOptions.md) - [IndexSuggestion](interfaces/IndexSuggestion.md) - [InsertConfig](interfaces/InsertConfig.md) +- [JoinOperator](interfaces/JoinOperator.md) - [LiveQueryCollectionConfig](interfaces/LiveQueryCollectionConfig.md) - [LiveQueryObserver](interfaces/LiveQueryObserver.md) - [LiveQuerySnapshot](interfaces/LiveQuerySnapshot.md) @@ -172,6 +177,7 @@ title: "@tanstack/db" - [Parser](interfaces/Parser.md) - [ParseWhereOptions](interfaces/ParseWhereOptions.md) - [PendingMutation](interfaces/PendingMutation.md) +- [PersistedInitialRenderInput](interfaces/PersistedInitialRenderInput.md) - [PersistedReadinessSource](interfaces/PersistedReadinessSource.md) - [QueryOnceConfig](interfaces/QueryOnceConfig.md) - [QueueStrategy](interfaces/QueueStrategy.md) @@ -250,6 +256,7 @@ title: "@tanstack/db" - [InsertMutationFn](type-aliases/InsertMutationFn.md) - [InsertMutationFnParams](type-aliases/InsertMutationFnParams.md) - [JoinOnCallback](type-aliases/JoinOnCallback.md) +- [JoinType](type-aliases/JoinType.md) - [KeyedNamespacedRow](type-aliases/KeyedNamespacedRow.md) - [KeyedStream](type-aliases/KeyedStream.md) - [LiveQueryCollectionUtils](type-aliases/LiveQueryCollectionUtils.md) @@ -277,6 +284,7 @@ title: "@tanstack/db" - [OptimisticChangeMessage](type-aliases/OptimisticChangeMessage.md) - [OrderByCallback](type-aliases/OrderByCallback.md) - [PersistedReadinessSnapshot](type-aliases/PersistedReadinessSnapshot.md) +- [PooledLiveQueries](type-aliases/PooledLiveQueries.md) - [Prettify](type-aliases/Prettify.md) - [QueryBuilder](type-aliases/QueryBuilder.md) - [QueryIdentity](type-aliases/QueryIdentity.md) @@ -326,9 +334,14 @@ title: "@tanstack/db" ## Variables - [collectionOptionsClaim](variables/collectionOptionsClaim.md) +- [fullJoin](variables/fullJoin.md) +- [innerJoin](variables/innerJoin.md) +- [leftJoin](variables/leftJoin.md) - [operators](variables/operators.md) - [PERSISTED\_READINESS](variables/PERSISTED_READINESS.md) +- [pooledLiveQueries](variables/pooledLiveQueries.md) - [Query](variables/Query.md) +- [rightJoin](variables/rightJoin.md) - [SYNC\_PERSISTENCE\_PROTOCOL](variables/SYNC_PERSISTENCE_PROTOCOL.md) - [SYNC\_PERSISTENCE\_VERSION](variables/SYNC_PERSISTENCE_VERSION.md) @@ -396,12 +409,14 @@ title: "@tanstack/db" - [getStableQueryBuilderHash](functions/getStableQueryBuilderHash.md) - [getStableQueryIRHash](functions/getStableQueryIRHash.md) - [getStableValueHash](functions/getStableValueHash.md) +- [groupKeys](functions/groupKeys.md) - [gt](functions/gt.md) - [gte](functions/gte.md) - [hasLiveQueryWindowLeases](functions/hasLiveQueryWindowLeases.md) - [hasVirtualProps](functions/hasVirtualProps.md) - [ilike](functions/ilike.md) - [inArray](functions/inArray.md) +- [include](functions/include.md) - [indexedDBCollectionOptions](functions/indexedDBCollectionOptions.md) - [isCollection](functions/isCollection.md) - [isCollectionOptions](functions/isCollectionOptions.md) @@ -410,6 +425,7 @@ title: "@tanstack/db" - [isNull](functions/isNull.md) - [isSingleResultCollection](functions/isSingleResultCollection.md) - [isUndefined](functions/isUndefined.md) +- [joinOf](functions/joinOf.md) - [length](functions/length.md) - [like](functions/like.md) - [liveQueryCollectionOptions](functions/liveQueryCollectionOptions.md) @@ -450,6 +466,7 @@ title: "@tanstack/db" - [trackQuery](functions/trackQuery.md) - [upper](functions/upper.md) - [validateSyncPersistenceCapability](functions/validateSyncPersistenceCapability.md) +- [waitForPersistedInitialRender](functions/waitForPersistedInitialRender.md) - [walkExpression](functions/walkExpression.md) - [warnOnce](functions/warnOnce.md) - [whenSyncAccepted](functions/whenSyncAccepted.md) diff --git a/docs/reference/interfaces/Context.md b/docs/reference/interfaces/Context.md index 124e3187ef..7c5bb19145 100644 --- a/docs/reference/interfaces/Context.md +++ b/docs/reference/interfaces/Context.md @@ -3,7 +3,7 @@ id: Context title: Context --- -Defined in: [packages/db/src/query/builder/types.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L47) +Defined in: [packages/db/src/query/builder/types.ts:48](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L48) Context - The central state container for query builder operations @@ -35,7 +35,7 @@ The context evolves through the query builder chain: baseSchema: ContextSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:49](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L49) +Defined in: [packages/db/src/query/builder/types.ts:50](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L50) *** @@ -45,7 +45,7 @@ Defined in: [packages/db/src/query/builder/types.ts:49](https://github.com/TanSt fromSourceName: string; ``` -Defined in: [packages/db/src/query/builder/types.ts:55](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L55) +Defined in: [packages/db/src/query/builder/types.ts:56](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L56) *** @@ -55,7 +55,7 @@ Defined in: [packages/db/src/query/builder/types.ts:55](https://github.com/TanSt optional fromSourceNames: readonly string[]; ``` -Defined in: [packages/db/src/query/builder/types.ts:57](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L57) +Defined in: [packages/db/src/query/builder/types.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L58) *** @@ -65,7 +65,7 @@ Defined in: [packages/db/src/query/builder/types.ts:57](https://github.com/TanSt optional hasJoins: boolean; ``` -Defined in: [packages/db/src/query/builder/types.ts:61](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L61) +Defined in: [packages/db/src/query/builder/types.ts:62](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L62) *** @@ -75,7 +75,7 @@ Defined in: [packages/db/src/query/builder/types.ts:61](https://github.com/TanSt optional hasResult: true; ``` -Defined in: [packages/db/src/query/builder/types.ts:70](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L70) +Defined in: [packages/db/src/query/builder/types.ts:71](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L71) *** @@ -85,7 +85,7 @@ Defined in: [packages/db/src/query/builder/types.ts:70](https://github.com/TanSt optional hasUnionFrom: true; ``` -Defined in: [packages/db/src/query/builder/types.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L59) +Defined in: [packages/db/src/query/builder/types.ts:60](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L60) *** @@ -95,7 +95,7 @@ Defined in: [packages/db/src/query/builder/types.ts:59](https://github.com/TanSt optional joinTypes: Record; ``` -Defined in: [packages/db/src/query/builder/types.ts:63](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L63) +Defined in: [packages/db/src/query/builder/types.ts:64](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L64) *** @@ -105,7 +105,7 @@ Defined in: [packages/db/src/query/builder/types.ts:63](https://github.com/TanSt optional refsSchema: ContextSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:53](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L53) +Defined in: [packages/db/src/query/builder/types.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L54) *** @@ -115,7 +115,7 @@ Defined in: [packages/db/src/query/builder/types.ts:53](https://github.com/TanSt optional result: any; ``` -Defined in: [packages/db/src/query/builder/types.ts:68](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L68) +Defined in: [packages/db/src/query/builder/types.ts:69](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L69) *** @@ -125,7 +125,7 @@ Defined in: [packages/db/src/query/builder/types.ts:68](https://github.com/TanSt schema: ContextSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:51](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L51) +Defined in: [packages/db/src/query/builder/types.ts:52](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L52) *** @@ -135,4 +135,4 @@ Defined in: [packages/db/src/query/builder/types.ts:51](https://github.com/TanSt optional singleResult: boolean; ``` -Defined in: [packages/db/src/query/builder/types.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L72) +Defined in: [packages/db/src/query/builder/types.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L73) diff --git a/docs/reference/interfaces/CreateLiveQueryObserverOptions.md b/docs/reference/interfaces/CreateLiveQueryObserverOptions.md index 4f5786ec86..f24417f203 100644 --- a/docs/reference/interfaces/CreateLiveQueryObserverOptions.md +++ b/docs/reference/interfaces/CreateLiveQueryObserverOptions.md @@ -3,7 +3,7 @@ id: CreateLiveQueryObserverOptions title: CreateLiveQueryObserverOptions --- -Defined in: [packages/db/src/live-query-observer.ts:1170](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1170) +Defined in: [packages/db/src/live-query-observer.ts:1098](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1098) ## Properties @@ -13,7 +13,7 @@ Defined in: [packages/db/src/live-query-observer.ts:1170](https://github.com/Tan optional client: DbClient; ``` -Defined in: [packages/db/src/live-query-observer.ts:1187](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1187) +Defined in: [packages/db/src/live-query-observer.ts:1115](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1115) DbClient cache that owns SSR snapshots for this query identity. @@ -25,7 +25,7 @@ DbClient cache that owns SSR snapshots for this query identity. optional mode: "granular" | "wholesale"; ``` -Defined in: [packages/db/src/live-query-observer.ts:1185](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1185) +Defined in: [packages/db/src/live-query-observer.ts:1113](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1113) How subscribers consume the observer: @@ -48,7 +48,7 @@ How subscribers consume the observer: optional onPreload: () => void; ``` -Defined in: [packages/db/src/live-query-observer.ts:1191](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1191) +Defined in: [packages/db/src/live-query-observer.ts:1119](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1119) Resume framework-deferred query sources before a server preload. @@ -64,6 +64,6 @@ Resume framework-deferred query sources before a server preload. optional queryHash: string; ``` -Defined in: [packages/db/src/live-query-observer.ts:1189](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1189) +Defined in: [packages/db/src/live-query-observer.ts:1117](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-observer.ts#L1117) Stable live-query identity used for dehydration and hydration. diff --git a/docs/reference/interfaces/GroupKeys.md b/docs/reference/interfaces/GroupKeys.md new file mode 100644 index 0000000000..e0c55ac254 --- /dev/null +++ b/docs/reference/interfaces/GroupKeys.md @@ -0,0 +1,132 @@ +--- +id: GroupKeys +title: GroupKeys +--- + +Defined in: [packages/db/src/query/builder/group-keys.ts:9](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/group-keys.ts#L9) + +Group keys for `query.groupBy(...)` when the query has no aggregate. +Aggregates such as `count` bring the grouping compiler themselves. + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](Context.md) + +## Properties + +### callback + +```ts +callback: GroupByCallback; +``` + +Defined in: [packages/db/src/query/builder/group-keys.ts:10](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/group-keys.ts#L10) + +*** + +### grouping + +```ts +grouping: object; +``` + +Defined in: [packages/db/src/query/builder/group-keys.ts:11](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/group-keys.ts#L11) + +#### processGroupBy() + +```ts +processGroupBy: (pipeline, groupByClause, valueIdentity, isSourceRef, havingClauses?, selectClause?, fnHavingClauses?, aggregateCollectionId?, mainSource?, sanitizeCallbackRows) => NamespacedAndKeyedStream; +``` + +Processes the GROUP BY clause with optional HAVING and SELECT +Works with the new $selected structure from early SELECT processing + +##### Parameters + +###### pipeline + +[`NamespacedAndKeyedStream`](../type-aliases/NamespacedAndKeyedStream.md) + +###### groupByClause + +[`GroupBy`](../@tanstack/namespaces/IR/type-aliases/GroupBy.md) + +###### valueIdentity + +`ValueIdentity` + +###### isSourceRef + +(`ref`) => `boolean` + +Whether a ref reads a field of the query's own rows. + +###### havingClauses? + +[`Where`](../@tanstack/namespaces/IR/type-aliases/Where.md)[] + +###### selectClause? + +[`Select`](../@tanstack/namespaces/IR/type-aliases/Select.md) + +###### fnHavingClauses? + +(`row`) => `any`[] + +###### aggregateCollectionId? + +`string` + +###### mainSource? + +`string` + +###### sanitizeCallbackRows? + +`boolean` = `false` + +##### Returns + +[`NamespacedAndKeyedStream`](../type-aliases/NamespacedAndKeyedStream.md) + +#### replaceAggregatesByRefs() + +```ts +replaceAggregatesByRefs: (havingExpr, selectClause, resultAlias) => BasicExpression; +``` + +Transforms expressions to replace aggregate functions with references to computed values. + +For aggregate expressions, finds matching aggregates in the SELECT clause and replaces them +with PropRef([resultAlias, alias]) to reference the computed aggregate value. + +Ref expressions (table columns and $selected fields) and value expressions are passed through unchanged. +Function expressions are recursively transformed. + +##### Parameters + +###### havingExpr + +The expression to transform (can be aggregate, ref, func, or val) + +[`Aggregate`](../@tanstack/namespaces/IR/classes/Aggregate.md)\<`any`\> | [`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md) + +###### selectClause + +[`Select`](../@tanstack/namespaces/IR/type-aliases/Select.md) + +The SELECT clause containing aliases and aggregate definitions + +###### resultAlias + +`string` = `...` + +The namespace alias for SELECT results (default: '$selected') + +##### Returns + +[`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md) + +A transformed BasicExpression that references computed values instead of raw expressions diff --git a/docs/reference/interfaces/JoinOperator.md b/docs/reference/interfaces/JoinOperator.md new file mode 100644 index 0000000000..11eac38a47 --- /dev/null +++ b/docs/reference/interfaces/JoinOperator.md @@ -0,0 +1,254 @@ +--- +id: JoinOperator +title: JoinOperator +--- + +Defined in: [packages/db/src/query/builder/join-operators.ts:19](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L19) + +A join step for `query.join(...)`. Importing a join operator is what brings +the join compiler into a bundle. + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](Context.md) + +### TSource + +`TSource` *extends* [`Source`](../type-aliases/Source.md) + +### TJoinType + +`TJoinType` *extends* [`JoinType`](../type-aliases/JoinType.md) + +## Properties + +### compile() + +```ts +compile: (pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, localFrom, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, valueIdentity, parentKeyStream?) => NamespacedAndKeyedStream; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:29](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L29) + +Processes all join clauses, applying lazy loading optimizations and maintaining +alias tracking for per-alias subscriptions (enables self-joins). + +#### Parameters + +##### pipeline + +[`NamespacedAndKeyedStream`](../type-aliases/NamespacedAndKeyedStream.md) + +##### joinClauses + +[`JoinClause`](../@tanstack/namespaces/IR/interfaces/JoinClause.md)[] + +##### sources + +`Record`\<`string`, [`KeyedStream`](../type-aliases/KeyedStream.md)\> + +##### mainCollectionId + +`string` + +##### mainSource + +`string` + +##### allInputs + +`Record`\<`string`, [`KeyedStream`](../type-aliases/KeyedStream.md)\> + +##### cache + +`QueryCache` + +##### queryMapping + +`QueryMapping` + +##### collections + +`Record`\<`string`, [`Collection`](Collection.md)\> + +##### subscriptions + +`Record`\<`string`, `CollectionSubscription`\> + +##### callbacks + +`Record`\<`string`, `LazyCollectionCallbacks`\> + +##### lazySources + +`Set`\<`string`\> + +##### optimizableOrderByCollections + +`Record`\<`string`, `OrderByOptimizationInfo`\> + +##### setWindowFn + +(`windowFn`) => `void` + +##### rawQuery + +[`QueryIR`](../@tanstack/namespaces/IR/interfaces/QueryIR.md) + +##### localFrom + +[`From`](../@tanstack/namespaces/IR/type-aliases/From.md) + +##### onCompileSubquery + +(...`args`) => `CompilationResult` + +##### aliasToCollectionId + +`Record`\<`string`, `string`\> + +##### aliasRemapping + +`Record`\<`string`, `string`\> + +##### sourceWhereClauses + +`Map`\<`string`, [`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md)\<`boolean`\>\> + +##### mainSourceIsParentFiltered + +`boolean` + +##### valueIdentity + +`ValueIdentity` + +##### parentKeyStream? + +[`KeyedStream`](../type-aliases/KeyedStream.md) + +#### Returns + +[`NamespacedAndKeyedStream`](../type-aliases/NamespacedAndKeyedStream.md) + +*** + +### materialize() + +```ts +materialize: (compilation, getRootKey?, reduceJoinedPublicKeys) => MaterializedRootCompilation; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:31](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L31) + +Compiles inline includes into the same D2 graph as their parent relation. +Collection-valued includes become inert bucket references. The public facade +adapter resolves those references after the graph reaches quiescence. + +#### Parameters + +##### compilation + +`CompilationResult` + +##### getRootKey? + +(`row`) => `unknown` + +##### reduceJoinedPublicKeys? + +`boolean` = `false` + +#### Returns + +`MaterializedRootCompilation` + +*** + +### on + +```ts +on: JoinOnCallback ? InferCollectionType : TSource[K] extends CollectionOptionsIdentity ? InferCollectionType : TSource[K] extends QueryBuilder ? ResultValue : never }[K] }>>; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:25](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L25) + +*** + +### optimize() + +```ts +optimize: (query, groupedClauses) => QueryIR; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:30](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L30) + +Step 4: Apply optimizations by lifting single-source clauses into subqueries. + +Creates a new QueryIR with single-source WHERE clauses moved to subqueries +that wrap the original table references. This ensures immutability and prevents +infinite recursion issues. + +#### Parameters + +##### query + +[`QueryIR`](../@tanstack/namespaces/IR/interfaces/QueryIR.md) + +Original QueryIR to optimize + +##### groupedClauses + +`GroupedWhereClauses` + +WHERE clauses grouped by optimization strategy + +#### Returns + +[`QueryIR`](../@tanstack/namespaces/IR/interfaces/QueryIR.md) + +New QueryIR with optimizations applied + +*** + +### source + +```ts +source: TSource; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L24) + +*** + +### type + +```ts +type: TJoinType; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L28) + +*** + +### validate() + +```ts +validate: (expression) => readonly [BasicExpression, BasicExpression][]; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:32](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L32) + +Validate join syntax and return its equality operands in source order. + +#### Parameters + +##### expression + +[`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md) + +#### Returns + +readonly \[[`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md), [`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md)\][] diff --git a/docs/reference/interfaces/LiveQueryCollectionConfig.md b/docs/reference/interfaces/LiveQueryCollectionConfig.md index 039753fffe..2b27dd4e56 100644 --- a/docs/reference/interfaces/LiveQueryCollectionConfig.md +++ b/docs/reference/interfaces/LiveQueryCollectionConfig.md @@ -3,7 +3,7 @@ id: LiveQueryCollectionConfig title: LiveQueryCollectionConfig --- -Defined in: [packages/db/src/query/live/types.ts:65](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L65) +Defined in: [packages/db/src/query/live/types.ts:66](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L66) Configuration interface for live query collection options @@ -47,7 +47,7 @@ const config: LiveQueryCollectionConfig = { optional defaultStringCollation: StringCollationConfig; ``` -Defined in: [packages/db/src/query/live/types.ts:121](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L121) +Defined in: [packages/db/src/query/live/types.ts:130](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L130) Optional compare options for string sorting. If provided, these will be used instead of inheriting from the FROM collection. @@ -60,7 +60,7 @@ If provided, these will be used instead of inheriting from the FROM collection. optional gcTime: number; ``` -Defined in: [packages/db/src/query/live/types.ts:110](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L110) +Defined in: [packages/db/src/query/live/types.ts:119](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L119) GC time for the collection @@ -72,7 +72,7 @@ GC time for the collection optional getKey: (item) => string | number; ``` -Defined in: [packages/db/src/query/live/types.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L88) +Defined in: [packages/db/src/query/live/types.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L97) Function to extract the key from result items If not provided, defaults to using the key from the D2 stream @@ -95,7 +95,7 @@ If not provided, defaults to using the key from the D2 stream optional id: string; ``` -Defined in: [packages/db/src/query/live/types.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L73) +Defined in: [packages/db/src/query/live/types.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L74) Unique identifier for the collection If not provided, defaults to `live-query-${number}` with auto-incrementing number @@ -110,7 +110,7 @@ optional onDelete: | DeleteMutationFn; ``` -Defined in: [packages/db/src/query/live/types.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L100) +Defined in: [packages/db/src/query/live/types.ts:109](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L109) *** @@ -122,7 +122,7 @@ optional onInsert: | InsertMutationFn; ``` -Defined in: [packages/db/src/query/live/types.ts:98](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L98) +Defined in: [packages/db/src/query/live/types.ts:107](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L107) Optional mutation handlers @@ -136,7 +136,70 @@ optional onUpdate: | UpdateMutationFn; ``` -Defined in: [packages/db/src/query/live/types.ts:99](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L99) +Defined in: [packages/db/src/query/live/types.ts:108](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L108) + +*** + +### pool? + +```ts +optional pool: object; +``` + +Defined in: [packages/db/src/query/live/types.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L82) + +Framework hooks only: pass `pooledLiveQueries` to serve this query from a +shared equality partition when its shape allows. A config with any option +other than `query`, `gcTime`, and `pool` compiles its own Collection. +`createLiveQueryCollection` does not pool. + +#### createPooledLiveQuery() + +```ts +createPooledLiveQuery: (query, __namedParameters) => + | Collection, any> + | undefined; +``` + +A pooled view for a query a partition can serve, or undefined. The view is +typed as the Collection it stands in for. + +##### Parameters + +###### query + +[`BaseQueryBuilder`](../classes/BaseQueryBuilder.md) + +###### \_\_namedParameters + +###### gcTime? + +`number` = `5_000` + +##### Returns + + \| [`Collection`](Collection.md)\<`any`, `any`, `any`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `any`\> + \| `undefined` + +#### getPooledQueryIdentity() + +```ts +getPooledQueryIdentity: (query) => string | undefined; +``` + +The identity of a query a partition can serve with no residual conjunct: +its source and its `eq` fields and literals, which determine its rows. +Undefined for any other query, which keeps the full structural identity. + +##### Parameters + +###### query + +[`BaseQueryBuilder`](../classes/BaseQueryBuilder.md) + +##### Returns + +`string` \| `undefined` *** @@ -148,7 +211,7 @@ query: | QueryBuilder & RootObjectResultConstraint; ``` -Defined in: [packages/db/src/query/live/types.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L78) +Defined in: [packages/db/src/query/live/types.ts:87](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L87) Query builder function that defines the live query @@ -160,7 +223,7 @@ Query builder function that defines the live query optional schema: undefined; ``` -Defined in: [packages/db/src/query/live/types.ts:93](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L93) +Defined in: [packages/db/src/query/live/types.ts:102](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L102) Optional schema for validation @@ -172,7 +235,7 @@ Optional schema for validation optional singleResult: true; ``` -Defined in: [packages/db/src/query/live/types.ts:115](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L115) +Defined in: [packages/db/src/query/live/types.ts:124](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L124) If enabled the collection will return a single object instead of an array @@ -184,6 +247,6 @@ If enabled the collection will return a single object instead of an array optional startSync: boolean; ``` -Defined in: [packages/db/src/query/live/types.ts:105](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L105) +Defined in: [packages/db/src/query/live/types.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L114) Start sync / the query immediately diff --git a/docs/reference/interfaces/PersistedInitialRenderInput.md b/docs/reference/interfaces/PersistedInitialRenderInput.md new file mode 100644 index 0000000000..9beff8ce0f --- /dev/null +++ b/docs/reference/interfaces/PersistedInitialRenderInput.md @@ -0,0 +1,118 @@ +--- +id: PersistedInitialRenderInput +title: PersistedInitialRenderInput +--- + +Defined in: [packages/db/src/persisted-readiness.ts:33](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L33) + +**`Internal`** + +What a live query observer passes to the initial-render wait. + +## Properties + +### clientQuery + +```ts +clientQuery: InitialRenderClientQuery | undefined; +``` + +Defined in: [packages/db/src/persisted-readiness.ts:41](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L41) + +The client query stream at the start of the wait, if any. + +*** + +### collectionStatus() + +```ts +collectionStatus: () => string | undefined; +``` + +Defined in: [packages/db/src/persisted-readiness.ts:45](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L45) + +The live query Collection's status now. + +#### Returns + +`string` \| `undefined` + +*** + +### currentClientQuery() + +```ts +currentClientQuery: () => unknown; +``` + +Defined in: [packages/db/src/persisted-readiness.ts:43](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L43) + +The client query stream now, for a failed stream's identity. + +#### Returns + +`unknown` + +*** + +### network + +```ts +network: Promise; +``` + +Defined in: [packages/db/src/persisted-readiness.ts:39](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L39) + +The network preload that this render prefers. + +*** + +### onReady() + +```ts +onReady: () => void; +``` + +Defined in: [packages/db/src/persisted-readiness.ts:48](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L48) + +Runs once the render may proceed, before the promise resolves. + +#### Returns + +`void` + +*** + +### readPersisted() + +```ts +readPersisted: () => object; +``` + +Defined in: [packages/db/src/persisted-readiness.ts:46](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L46) + +#### Returns + +`object` + +##### error + +```ts +error: unknown; +``` + +##### status + +```ts +status: string; +``` + +*** + +### sources + +```ts +sources: readonly object[]; +``` + +Defined in: [packages/db/src/persisted-readiness.ts:34](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L34) diff --git a/docs/reference/interfaces/PersistedReadinessSource.md b/docs/reference/interfaces/PersistedReadinessSource.md index af76af3e36..2de10bd6ed 100644 --- a/docs/reference/interfaces/PersistedReadinessSource.md +++ b/docs/reference/interfaces/PersistedReadinessSource.md @@ -3,7 +3,7 @@ id: PersistedReadinessSource title: PersistedReadinessSource --- -Defined in: [packages/db/src/persisted-readiness.ts:8](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L8) +Defined in: [packages/db/src/persisted-readiness.ts:10](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L10) **`Internal`** @@ -17,7 +17,7 @@ Capability supplied by a persistence adapter, not by core sync. getOrStartNetworkDeadline: () => number; ``` -Defined in: [packages/db/src/persisted-readiness.ts:12](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L12) +Defined in: [packages/db/src/persisted-readiness.ts:14](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L14) Anchor the deadline to this Collection's current sync run. @@ -33,7 +33,7 @@ Anchor the deadline to this Collection's current sync run. getSnapshot: () => PersistedReadinessSnapshot; ``` -Defined in: [packages/db/src/persisted-readiness.ts:13](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L13) +Defined in: [packages/db/src/persisted-readiness.ts:15](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L15) #### Returns @@ -47,7 +47,7 @@ Defined in: [packages/db/src/persisted-readiness.ts:13](https://github.com/TanSt networkTimeoutMs: number; ``` -Defined in: [packages/db/src/persisted-readiness.ts:10](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L10) +Defined in: [packages/db/src/persisted-readiness.ts:12](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L12) Time to prefer network before a completed persisted restore may render. @@ -59,7 +59,7 @@ Time to prefer network before a completed persisted restore may render. subscribe: (listener) => () => void; ``` -Defined in: [packages/db/src/persisted-readiness.ts:14](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L14) +Defined in: [packages/db/src/persisted-readiness.ts:16](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L16) #### Parameters @@ -76,3 +76,47 @@ Defined in: [packages/db/src/persisted-readiness.ts:14](https://github.com/TanSt ##### Returns `void` + +*** + +### waitForInitialRender() + +```ts +waitForInitialRender: (__namedParameters) => object; +``` + +Defined in: [packages/db/src/persisted-readiness.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L22) + +The network-first initial-render wait, normally +`waitForPersistedInitialRender`. A persistence adapter supplies it with +its readiness, so an app with no persisted Collection does not ship it. + +Resolve when network data is ready, or when the network deadline passes or +the network fails while every persisted source has restored. Reject with +the network error when no persisted fallback may render. + +#### Parameters + +##### \_\_namedParameters + +[`PersistedInitialRenderInput`](PersistedInitialRenderInput.md) + +#### Returns + +`object` + +##### cancel() + +```ts +cancel: () => void; +``` + +###### Returns + +`void` + +##### promise + +```ts +promise: Promise; +``` diff --git a/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md b/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md index 694de11907..bbec18bb8b 100644 --- a/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md +++ b/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md @@ -7,7 +7,7 @@ title: ApplyJoinOptionalityToMergedSchema type ApplyJoinOptionalityToMergedSchema = { [K in keyof TExistingSchema]: K extends TFromSourceNames ? TJoinType extends "right" | "full" ? TExistingSchema[K] | undefined : TExistingSchema[K] : TExistingSchema[K] } & { [K in keyof TNewSchema]: TJoinType extends "left" | "full" ? TNewSchema[K] | undefined : TNewSchema[K] }; ``` -Defined in: [packages/db/src/query/builder/types.ts:1037](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1037) +Defined in: [packages/db/src/query/builder/types.ts:1038](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1038) ApplyJoinOptionalityToMergedSchema - Applies optionality rules when merging schemas diff --git a/docs/reference/type-aliases/ContextFromSource.md b/docs/reference/type-aliases/ContextFromSource.md index 7331163899..538bb54705 100644 --- a/docs/reference/type-aliases/ContextFromSource.md +++ b/docs/reference/type-aliases/ContextFromSource.md @@ -7,7 +7,7 @@ title: ContextFromSource type ContextFromSource = object; ``` -Defined in: [packages/db/src/query/builder/types.ts:154](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L154) +Defined in: [packages/db/src/query/builder/types.ts:155](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L155) ## Type Parameters @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/builder/types.ts:154](https://github.com/TanS baseSchema: SchemaFromSource; ``` -Defined in: [packages/db/src/query/builder/types.ts:155](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L155) +Defined in: [packages/db/src/query/builder/types.ts:156](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L156) *** @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/types.ts:155](https://github.com/TanS fromSourceName: keyof TSource & string; ``` -Defined in: [packages/db/src/query/builder/types.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L157) +Defined in: [packages/db/src/query/builder/types.ts:158](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L158) *** @@ -43,7 +43,7 @@ Defined in: [packages/db/src/query/builder/types.ts:157](https://github.com/TanS hasJoins: false; ``` -Defined in: [packages/db/src/query/builder/types.ts:158](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L158) +Defined in: [packages/db/src/query/builder/types.ts:159](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L159) *** @@ -53,4 +53,4 @@ Defined in: [packages/db/src/query/builder/types.ts:158](https://github.com/TanS schema: SchemaFromSource; ``` -Defined in: [packages/db/src/query/builder/types.ts:156](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L156) +Defined in: [packages/db/src/query/builder/types.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L157) diff --git a/docs/reference/type-aliases/ContextFromUnionBranches.md b/docs/reference/type-aliases/ContextFromUnionBranches.md index 84c7931b1c..72dd0a7515 100644 --- a/docs/reference/type-aliases/ContextFromUnionBranches.md +++ b/docs/reference/type-aliases/ContextFromUnionBranches.md @@ -7,7 +7,7 @@ title: ContextFromUnionBranches type ContextFromUnionBranches = object; ``` -Defined in: [packages/db/src/query/builder/types.ts:188](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L188) +Defined in: [packages/db/src/query/builder/types.ts:189](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L189) ## Type Parameters @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/builder/types.ts:188](https://github.com/TanS [BranchUnionRefs]: UnionBranchResult; ``` -Defined in: [packages/db/src/query/builder/types.ts:198](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L198) +Defined in: [packages/db/src/query/builder/types.ts:199](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L199) *** @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/types.ts:198](https://github.com/TanS baseSchema: UnionBranchSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:191](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L191) +Defined in: [packages/db/src/query/builder/types.ts:192](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L192) *** @@ -43,7 +43,7 @@ Defined in: [packages/db/src/query/builder/types.ts:191](https://github.com/TanS fromSourceName: keyof UnionBranchSchema & string; ``` -Defined in: [packages/db/src/query/builder/types.ts:194](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L194) +Defined in: [packages/db/src/query/builder/types.ts:195](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L195) *** @@ -53,7 +53,7 @@ Defined in: [packages/db/src/query/builder/types.ts:194](https://github.com/TanS hasJoins: false; ``` -Defined in: [packages/db/src/query/builder/types.ts:195](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L195) +Defined in: [packages/db/src/query/builder/types.ts:196](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L196) *** @@ -63,7 +63,7 @@ Defined in: [packages/db/src/query/builder/types.ts:195](https://github.com/TanS hasResult: true; ``` -Defined in: [packages/db/src/query/builder/types.ts:197](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L197) +Defined in: [packages/db/src/query/builder/types.ts:198](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L198) *** @@ -73,7 +73,7 @@ Defined in: [packages/db/src/query/builder/types.ts:197](https://github.com/TanS refsSchema: UnionBranchSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L193) +Defined in: [packages/db/src/query/builder/types.ts:194](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L194) *** @@ -83,7 +83,7 @@ Defined in: [packages/db/src/query/builder/types.ts:193](https://github.com/TanS result: PrettifyIfPlainObject>; ``` -Defined in: [packages/db/src/query/builder/types.ts:196](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L196) +Defined in: [packages/db/src/query/builder/types.ts:197](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L197) *** @@ -93,4 +93,4 @@ Defined in: [packages/db/src/query/builder/types.ts:196](https://github.com/TanS schema: UnionBranchSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:192](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L192) +Defined in: [packages/db/src/query/builder/types.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L193) diff --git a/docs/reference/type-aliases/ContextFromUnionSource.md b/docs/reference/type-aliases/ContextFromUnionSource.md index 34c1c1f710..50b1cd223f 100644 --- a/docs/reference/type-aliases/ContextFromUnionSource.md +++ b/docs/reference/type-aliases/ContextFromUnionSource.md @@ -7,7 +7,7 @@ title: ContextFromUnionSource type ContextFromUnionSource = IsUnion extends true ? object : ContextFromSource; ``` -Defined in: [packages/db/src/query/builder/types.ts:161](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L161) +Defined in: [packages/db/src/query/builder/types.ts:162](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L162) ## Type Parameters diff --git a/docs/reference/type-aliases/ContextSchema.md b/docs/reference/type-aliases/ContextSchema.md index 7e0592623c..b71ea52f8c 100644 --- a/docs/reference/type-aliases/ContextSchema.md +++ b/docs/reference/type-aliases/ContextSchema.md @@ -7,7 +7,7 @@ title: ContextSchema type ContextSchema = Record; ``` -Defined in: [packages/db/src/query/builder/types.ts:84](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L84) +Defined in: [packages/db/src/query/builder/types.ts:85](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L85) ContextSchema - The shape of available tables/collections in a query context diff --git a/docs/reference/type-aliases/DeferredLiveQueryCollections.md b/docs/reference/type-aliases/DeferredLiveQueryCollections.md index 5e3895f3a2..10ff5b3b98 100644 --- a/docs/reference/type-aliases/DeferredLiveQueryCollections.md +++ b/docs/reference/type-aliases/DeferredLiveQueryCollections.md @@ -7,4 +7,4 @@ title: DeferredLiveQueryCollections type DeferredLiveQueryCollections = Set>; ``` -Defined in: [packages/db/src/live-query-options.ts:31](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L31) +Defined in: [packages/db/src/live-query-options.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L28) diff --git a/docs/reference/type-aliases/ExtractContext.md b/docs/reference/type-aliases/ExtractContext.md index 2ae5f6d96a..1a96c351e8 100644 --- a/docs/reference/type-aliases/ExtractContext.md +++ b/docs/reference/type-aliases/ExtractContext.md @@ -7,7 +7,7 @@ title: ExtractContext type ExtractContext = T extends BaseQueryBuilder ? TContext : T extends QueryBuilder ? TContext : never; ``` -Defined in: [packages/db/src/query/builder/index.ts:1802](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1802) +Defined in: [packages/db/src/query/builder/index.ts:1765](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1765) ## Type Parameters diff --git a/docs/reference/type-aliases/FunctionalHavingRow.md b/docs/reference/type-aliases/FunctionalHavingRow.md index 08594c36ad..a3ea02343b 100644 --- a/docs/reference/type-aliases/FunctionalHavingRow.md +++ b/docs/reference/type-aliases/FunctionalHavingRow.md @@ -7,7 +7,7 @@ title: FunctionalHavingRow type FunctionalHavingRow = TContext["schema"] & TContext["hasResult"] extends true ? object : object; ``` -Defined in: [packages/db/src/query/builder/types.ts:662](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L662) +Defined in: [packages/db/src/query/builder/types.ts:663](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L663) FunctionalHavingRow - Type for the row parameter in functional having callbacks diff --git a/docs/reference/type-aliases/GetResult.md b/docs/reference/type-aliases/GetResult.md index 0e82d3f63b..6bb3175ef3 100644 --- a/docs/reference/type-aliases/GetResult.md +++ b/docs/reference/type-aliases/GetResult.md @@ -7,7 +7,7 @@ title: GetResult type GetResult = Prettify>; ``` -Defined in: [packages/db/src/query/builder/types.ts:1171](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1171) +Defined in: [packages/db/src/query/builder/types.ts:1172](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1172) ## Type Parameters diff --git a/docs/reference/type-aliases/GroupByCallback.md b/docs/reference/type-aliases/GroupByCallback.md index fd4a00f164..66d5cfd4d0 100644 --- a/docs/reference/type-aliases/GroupByCallback.md +++ b/docs/reference/type-aliases/GroupByCallback.md @@ -7,7 +7,7 @@ title: GroupByCallback type GroupByCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:625](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L625) +Defined in: [packages/db/src/query/builder/types.ts:626](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L626) GroupByCallback - Type for groupBy clause callback functions diff --git a/docs/reference/type-aliases/InferCollectionType.md b/docs/reference/type-aliases/InferCollectionType.md index 118b077e7b..4a835008bd 100644 --- a/docs/reference/type-aliases/InferCollectionType.md +++ b/docs/reference/type-aliases/InferCollectionType.md @@ -7,7 +7,7 @@ title: InferCollectionType type InferCollectionType = T extends CollectionImpl ? WithVirtualProps : T extends CollectionOptionsIdentity ? WithVirtualProps : never; ``` -Defined in: [packages/db/src/query/builder/types.ts:108](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L108) +Defined in: [packages/db/src/query/builder/types.ts:109](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L109) InferCollectionType - Extracts the TypeScript type from a CollectionImpl diff --git a/docs/reference/type-aliases/InferResultType.md b/docs/reference/type-aliases/InferResultType.md index 60c2fcb1be..fea31ee06c 100644 --- a/docs/reference/type-aliases/InferResultType.md +++ b/docs/reference/type-aliases/InferResultType.md @@ -7,7 +7,7 @@ title: InferResultType type InferResultType = TContext extends SingleResult ? GetResult | undefined : GetResult[]; ``` -Defined in: [packages/db/src/query/builder/types.ts:1063](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1063) +Defined in: [packages/db/src/query/builder/types.ts:1064](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1064) Utility type to infer the query result size (single row or an array) diff --git a/docs/reference/type-aliases/InitialQueryBuilder.md b/docs/reference/type-aliases/InitialQueryBuilder.md index ea48982c4d..a8a43e2c62 100644 --- a/docs/reference/type-aliases/InitialQueryBuilder.md +++ b/docs/reference/type-aliases/InitialQueryBuilder.md @@ -7,4 +7,4 @@ title: InitialQueryBuilder type InitialQueryBuilder = Pick, "from" | "unionAll">; ``` -Defined in: [packages/db/src/query/builder/index.ts:1785](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1785) +Defined in: [packages/db/src/query/builder/index.ts:1748](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1748) diff --git a/docs/reference/type-aliases/JoinOnCallback.md b/docs/reference/type-aliases/JoinOnCallback.md index 5aa8093c06..de3428e372 100644 --- a/docs/reference/type-aliases/JoinOnCallback.md +++ b/docs/reference/type-aliases/JoinOnCallback.md @@ -7,7 +7,7 @@ title: JoinOnCallback type JoinOnCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:641](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L641) +Defined in: [packages/db/src/query/builder/types.ts:642](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L642) JoinOnCallback - Type for join condition callback functions diff --git a/docs/reference/type-aliases/JoinType.md b/docs/reference/type-aliases/JoinType.md new file mode 100644 index 0000000000..a7868ad281 --- /dev/null +++ b/docs/reference/type-aliases/JoinType.md @@ -0,0 +1,10 @@ +--- +id: JoinType +title: JoinType +--- + +```ts +type JoinType = "inner" | "left" | "right" | "full"; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:13](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L13) diff --git a/docs/reference/type-aliases/LiveQueryCollectionUtils.md b/docs/reference/type-aliases/LiveQueryCollectionUtils.md index 0c6c5236ea..97d800ad0c 100644 --- a/docs/reference/type-aliases/LiveQueryCollectionUtils.md +++ b/docs/reference/type-aliases/LiveQueryCollectionUtils.md @@ -7,7 +7,7 @@ title: LiveQueryCollectionUtils type LiveQueryCollectionUtils = UtilsRecord & object; ``` -Defined in: [packages/db/src/query/live/collection-config-builder.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/collection-config-builder.ts#L58) +Defined in: [packages/db/src/query/live/collection-config-builder.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/collection-config-builder.ts#L59) ## Type Declaration diff --git a/docs/reference/type-aliases/LiveQueryKey.md b/docs/reference/type-aliases/LiveQueryKey.md index e287f35b36..76869a8605 100644 --- a/docs/reference/type-aliases/LiveQueryKey.md +++ b/docs/reference/type-aliases/LiveQueryKey.md @@ -7,4 +7,4 @@ title: LiveQueryKey type LiveQueryKey = ReadonlyArray; ``` -Defined in: [packages/db/src/live-query-options.ts:25](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L25) +Defined in: [packages/db/src/live-query-options.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L22) diff --git a/docs/reference/type-aliases/LiveQueryOptions.md b/docs/reference/type-aliases/LiveQueryOptions.md index c96a4c1f89..3232d637b7 100644 --- a/docs/reference/type-aliases/LiveQueryOptions.md +++ b/docs/reference/type-aliases/LiveQueryOptions.md @@ -7,7 +7,7 @@ title: LiveQueryOptions type LiveQueryOptions = LiveQueryCollectionConfig & object; ``` -Defined in: [packages/db/src/live-query-options.ts:27](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L27) +Defined in: [packages/db/src/live-query-options.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L24) ## Type Declaration diff --git a/docs/reference/type-aliases/MergeContextForJoinCallback.md b/docs/reference/type-aliases/MergeContextForJoinCallback.md index 35a01bf8f7..fe7f9ab8f1 100644 --- a/docs/reference/type-aliases/MergeContextForJoinCallback.md +++ b/docs/reference/type-aliases/MergeContextForJoinCallback.md @@ -7,7 +7,7 @@ title: MergeContextForJoinCallback type MergeContextForJoinCallback = Omit & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:1328](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1328) +Defined in: [packages/db/src/query/builder/types.ts:1329](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1329) MergeContextForJoinCallback - Special context for join condition callbacks @@ -22,11 +22,11 @@ Optionality is only applied to the result AFTER the join logic executes. **Example**: ```typescript .from({ users }) -.leftJoin({ orders }, ({ users, orders }) => { +.join(leftJoin({ orders }, ({ users, orders }) => { // users is NOT optional here - we can access users.id directly // orders is NOT optional here - we can access orders.userId directly return eq(users.id, orders.userId) -}) +})) .where(({ orders }) => { // NOW orders is optional because it's after the LEFT JOIN return orders?.status === 'pending' diff --git a/docs/reference/type-aliases/MergeContextWithJoinType.md b/docs/reference/type-aliases/MergeContextWithJoinType.md index 77505e1816..dbb5e5bbca 100644 --- a/docs/reference/type-aliases/MergeContextWithJoinType.md +++ b/docs/reference/type-aliases/MergeContextWithJoinType.md @@ -7,7 +7,7 @@ title: MergeContextWithJoinType type MergeContextWithJoinType = Omit & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:983](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L983) +Defined in: [packages/db/src/query/builder/types.ts:984](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L984) MergeContextWithJoinType - Creates a new context after a join operation diff --git a/docs/reference/type-aliases/OrderByCallback.md b/docs/reference/type-aliases/OrderByCallback.md index 0210ce829c..a45c3fee41 100644 --- a/docs/reference/type-aliases/OrderByCallback.md +++ b/docs/reference/type-aliases/OrderByCallback.md @@ -7,7 +7,7 @@ title: OrderByCallback type OrderByCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:589](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L589) +Defined in: [packages/db/src/query/builder/types.ts:590](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L590) OrderByCallback - Type for orderBy clause callback functions diff --git a/docs/reference/type-aliases/PersistedReadinessSnapshot.md b/docs/reference/type-aliases/PersistedReadinessSnapshot.md index ab8ec1d61a..be0fb73fd6 100644 --- a/docs/reference/type-aliases/PersistedReadinessSnapshot.md +++ b/docs/reference/type-aliases/PersistedReadinessSnapshot.md @@ -19,6 +19,6 @@ type PersistedReadinessSnapshot = }; ``` -Defined in: [packages/db/src/persisted-readiness.ts:2](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L2) +Defined in: [packages/db/src/persisted-readiness.ts:4](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L4) The local-restore boundary of one opted-in persisted Collection. diff --git a/docs/reference/type-aliases/PooledLiveQueries.md b/docs/reference/type-aliases/PooledLiveQueries.md new file mode 100644 index 0000000000..4ddc740a5e --- /dev/null +++ b/docs/reference/type-aliases/PooledLiveQueries.md @@ -0,0 +1,10 @@ +--- +id: PooledLiveQueries +title: PooledLiveQueries +--- + +```ts +type PooledLiveQueries = typeof pooledLiveQueries; +``` + +Defined in: [packages/db/src/query/pooled-live-query.ts:735](https://github.com/TanStack/db/blob/main/packages/db/src/query/pooled-live-query.ts#L735) diff --git a/docs/reference/type-aliases/Prettify.md b/docs/reference/type-aliases/Prettify.md index 9999dd635b..9422fdce71 100644 --- a/docs/reference/type-aliases/Prettify.md +++ b/docs/reference/type-aliases/Prettify.md @@ -7,7 +7,7 @@ title: Prettify type Prettify = { [K in keyof T]: T[K] } & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:1370](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1370) +Defined in: [packages/db/src/query/builder/types.ts:1371](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1371) Prettify - Utility type for clean IDE display diff --git a/docs/reference/type-aliases/QueryBuilder.md b/docs/reference/type-aliases/QueryBuilder.md index 789467f2e0..d54cb4c8b4 100644 --- a/docs/reference/type-aliases/QueryBuilder.md +++ b/docs/reference/type-aliases/QueryBuilder.md @@ -7,7 +7,7 @@ title: QueryBuilder type QueryBuilder = Omit, "from" | "unionAll" | "_getQuery">; ``` -Defined in: [packages/db/src/query/builder/index.ts:1792](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1792) +Defined in: [packages/db/src/query/builder/index.ts:1755](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1755) ## Type Parameters diff --git a/docs/reference/type-aliases/QueryResult.md b/docs/reference/type-aliases/QueryResult.md index 68bb6da2c5..307b7ec106 100644 --- a/docs/reference/type-aliases/QueryResult.md +++ b/docs/reference/type-aliases/QueryResult.md @@ -7,7 +7,7 @@ title: QueryResult type QueryResult = GetResult>; ``` -Defined in: [packages/db/src/query/builder/index.ts:1810](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1810) +Defined in: [packages/db/src/query/builder/index.ts:1773](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1773) ## Type Parameters diff --git a/docs/reference/type-aliases/Ref.md b/docs/reference/type-aliases/Ref.md index 77e88812f2..dbc8d4025e 100644 --- a/docs/reference/type-aliases/Ref.md +++ b/docs/reference/type-aliases/Ref.md @@ -7,7 +7,7 @@ title: Ref type Ref = T extends unknown ? RefBranch : never; ``` -Defined in: [packages/db/src/query/builder/types.ts:899](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L899) +Defined in: [packages/db/src/query/builder/types.ts:900](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L900) Ref - The user-facing ref interface for the query builder diff --git a/docs/reference/type-aliases/RefsForContext.md b/docs/reference/type-aliases/RefsForContext.md index b3c59189e0..c7e9057ec9 100644 --- a/docs/reference/type-aliases/RefsForContext.md +++ b/docs/reference/type-aliases/RefsForContext.md @@ -7,7 +7,7 @@ title: RefsForContext type RefsForContext = { [K in Exclude>, keyof JoinedRefsForContext | keyof BranchUnionResultRefs>]: RefForContextSchemaValue, K>, IsNullableContextKey> } & TContext["hasResult"] extends true ? object : object & BranchUnionResultRefs & JoinedRefsForContext; ``` -Defined in: [packages/db/src/query/builder/types.ts:761](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L761) +Defined in: [packages/db/src/query/builder/types.ts:762](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L762) ## Type Parameters diff --git a/docs/reference/type-aliases/ResultTypeFromSelect.md b/docs/reference/type-aliases/ResultTypeFromSelect.md index 00f2132fc4..f52d8d7c8f 100644 --- a/docs/reference/type-aliases/ResultTypeFromSelect.md +++ b/docs/reference/type-aliases/ResultTypeFromSelect.md @@ -7,7 +7,7 @@ title: ResultTypeFromSelect type ResultTypeFromSelect = IsAny extends true ? any : IsTrueRef extends true ? ExtractDirectSelectRef : WithoutRefBrand extends true ? ExtractExpressionType : TSelectObject[K] extends ToArrayWrapper ? T[] : TSelectObject[K] extends ConcatToArrayWrapper ? string : TSelectObject[K] extends MaterializeWrapper ? IsSingle extends true ? (...) | (...) : (...)[] : (...)[(...)] extends { __brand: ...; _result?: ... } ? ResultTypeFromCaseWhen<(...)> : (...) extends (...) ? (...) : (...) }>>; ``` -Defined in: [packages/db/src/query/builder/types.ts:413](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L413) +Defined in: [packages/db/src/query/builder/types.ts:414](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L414) ResultTypeFromSelect - Infers the result type from a select object diff --git a/docs/reference/type-aliases/SchemaFromSource.md b/docs/reference/type-aliases/SchemaFromSource.md index 64f52563e8..2fff2d490f 100644 --- a/docs/reference/type-aliases/SchemaFromSource.md +++ b/docs/reference/type-aliases/SchemaFromSource.md @@ -7,7 +7,7 @@ title: SchemaFromSource type SchemaFromSource = Prettify<{ [K in keyof T]: T[K] extends CollectionImpl ? InferCollectionType : T[K] extends CollectionOptionsIdentity ? InferCollectionType : T[K] extends QueryBuilder ? GetRawResult : never }>; ``` -Defined in: [packages/db/src/query/builder/types.ts:131](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L131) +Defined in: [packages/db/src/query/builder/types.ts:132](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L132) SchemaFromSource - Converts a Source definition into a ContextSchema diff --git a/docs/reference/type-aliases/SelectObject.md b/docs/reference/type-aliases/SelectObject.md index 1cd3cfc774..3acca6c988 100644 --- a/docs/reference/type-aliases/SelectObject.md +++ b/docs/reference/type-aliases/SelectObject.md @@ -7,7 +7,7 @@ title: SelectObject type SelectObject = T; ``` -Defined in: [packages/db/src/query/builder/types.ts:303](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L303) +Defined in: [packages/db/src/query/builder/types.ts:304](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L304) SelectObject - Wrapper type for select clause objects diff --git a/docs/reference/type-aliases/SingleSource.md b/docs/reference/type-aliases/SingleSource.md index 846101c262..4d67707522 100644 --- a/docs/reference/type-aliases/SingleSource.md +++ b/docs/reference/type-aliases/SingleSource.md @@ -7,7 +7,7 @@ title: SingleSource type SingleSource = IsUnion extends true ? never : TSource; ``` -Defined in: [packages/db/src/query/builder/types.ts:151](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L151) +Defined in: [packages/db/src/query/builder/types.ts:152](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L152) ## Type Parameters diff --git a/docs/reference/type-aliases/Source.md b/docs/reference/type-aliases/Source.md index 5b3d8d1594..16190f9745 100644 --- a/docs/reference/type-aliases/Source.md +++ b/docs/reference/type-aliases/Source.md @@ -7,7 +7,7 @@ title: Source type Source = object; ``` -Defined in: [packages/db/src/query/builder/types.ts:95](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L95) +Defined in: [packages/db/src/query/builder/types.ts:96](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L96) Source - Input definition for query builder `from()` and `unionAll()` clauses @@ -21,7 +21,7 @@ Example: `{ users: usersCollection }` ```ts [alias: string]: - | CollectionOptionsIdentity | QueryBuilder + | CollectionOptionsIdentity | CollectionImpl ``` diff --git a/docs/reference/type-aliases/SourceClauseContext.md b/docs/reference/type-aliases/SourceClauseContext.md index d1d13a0415..3636ce95ee 100644 --- a/docs/reference/type-aliases/SourceClauseContext.md +++ b/docs/reference/type-aliases/SourceClauseContext.md @@ -7,4 +7,4 @@ title: SourceClauseContext type SourceClauseContext = "from clause" | "unionAll clause" | "join clause"; ``` -Defined in: [packages/db/src/errors.ts:613](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L613) +Defined in: [packages/db/src/errors.ts:633](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L633) diff --git a/docs/reference/type-aliases/WhereCallback.md b/docs/reference/type-aliases/WhereCallback.md index 0f5a10e44f..f383b83bd1 100644 --- a/docs/reference/type-aliases/WhereCallback.md +++ b/docs/reference/type-aliases/WhereCallback.md @@ -7,7 +7,7 @@ title: WhereCallback type WhereCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:218](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L218) +Defined in: [packages/db/src/query/builder/types.ts:219](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L219) WhereCallback - Type for where/having clause callback functions diff --git a/docs/reference/type-aliases/WithResult.md b/docs/reference/type-aliases/WithResult.md index 687f0b2fcc..6b8d2ead42 100644 --- a/docs/reference/type-aliases/WithResult.md +++ b/docs/reference/type-aliases/WithResult.md @@ -7,7 +7,7 @@ title: WithResult type WithResult = Prettify & Pick & object>; ``` -Defined in: [packages/db/src/query/builder/types.ts:1359](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1359) +Defined in: [packages/db/src/query/builder/types.ts:1360](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1360) WithResult - Updates a context with a new result type after select() diff --git a/docs/reference/variables/PERSISTED_READINESS.md b/docs/reference/variables/PERSISTED_READINESS.md index 9867fc89cb..2bafd57ff6 100644 --- a/docs/reference/variables/PERSISTED_READINESS.md +++ b/docs/reference/variables/PERSISTED_READINESS.md @@ -7,7 +7,7 @@ title: PERSISTED_READINESS const PERSISTED_READINESS: typeof PERSISTED_READINESS; ``` -Defined in: [packages/db/src/persisted-readiness.ts:18](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L18) +Defined in: [packages/db/src/persisted-readiness.ts:158](https://github.com/TanStack/db/blob/main/packages/db/src/persisted-readiness.ts#L158) **`Internal`** diff --git a/docs/reference/variables/Query.md b/docs/reference/variables/Query.md index df58acceb8..06c6923357 100644 --- a/docs/reference/variables/Query.md +++ b/docs/reference/variables/Query.md @@ -7,4 +7,4 @@ title: Query const Query: InitialQueryBuilderConstructor = BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:1799](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1799) +Defined in: [packages/db/src/query/builder/index.ts:1762](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1762) diff --git a/docs/reference/variables/fullJoin.md b/docs/reference/variables/fullJoin.md new file mode 100644 index 0000000000..5a53de6f1b --- /dev/null +++ b/docs/reference/variables/fullJoin.md @@ -0,0 +1,34 @@ +--- +id: fullJoin +title: fullJoin +--- + +```ts +const fullJoin: (source, on) => JoinOperator; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:55](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L55) + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) + +### TSource + +`TSource` *extends* [`Source`](../type-aliases/Source.md) + +## Parameters + +### source + +`TSource` + +### on + +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> + +## Returns + +[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `"full"`\> diff --git a/docs/reference/variables/innerJoin.md b/docs/reference/variables/innerJoin.md new file mode 100644 index 0000000000..a6ff95ec1e --- /dev/null +++ b/docs/reference/variables/innerJoin.md @@ -0,0 +1,34 @@ +--- +id: innerJoin +title: innerJoin +--- + +```ts +const innerJoin: (source, on) => JoinOperator; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L54) + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) + +### TSource + +`TSource` *extends* [`Source`](../type-aliases/Source.md) + +## Parameters + +### source + +`TSource` + +### on + +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> + +## Returns + +[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `"inner"`\> diff --git a/docs/reference/variables/leftJoin.md b/docs/reference/variables/leftJoin.md new file mode 100644 index 0000000000..3112accde7 --- /dev/null +++ b/docs/reference/variables/leftJoin.md @@ -0,0 +1,34 @@ +--- +id: leftJoin +title: leftJoin +--- + +```ts +const leftJoin: (source, on) => JoinOperator; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:52](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L52) + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) + +### TSource + +`TSource` *extends* [`Source`](../type-aliases/Source.md) + +## Parameters + +### source + +`TSource` + +### on + +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> + +## Returns + +[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `"left"`\> diff --git a/docs/reference/variables/pooledLiveQueries.md b/docs/reference/variables/pooledLiveQueries.md new file mode 100644 index 0000000000..02b566e2f7 --- /dev/null +++ b/docs/reference/variables/pooledLiveQueries.md @@ -0,0 +1,64 @@ +--- +id: pooledLiveQueries +title: pooledLiveQueries +--- + +```ts +const pooledLiveQueries: object; +``` + +Defined in: [packages/db/src/query/pooled-live-query.ts:733](https://github.com/TanStack/db/blob/main/packages/db/src/query/pooled-live-query.ts#L733) + +Pooled live queries for framework hooks. Pass it as `pool` in a hook's +query config to serve equality-filtered queries of one shape from a shared +partition. Apps that do not pass it do not bundle pooling. + +## Type Declaration + +### createPooledLiveQuery() + +```ts +createPooledLiveQuery: (query, __namedParameters) => + | Collection, any> + | undefined; +``` + +A pooled view for a query a partition can serve, or undefined. The view is +typed as the Collection it stands in for. + +#### Parameters + +##### query + +[`BaseQueryBuilder`](../classes/BaseQueryBuilder.md) + +##### \_\_namedParameters + +###### gcTime? + +`number` = `5_000` + +#### Returns + + \| [`Collection`](../interfaces/Collection.md)\<`any`, `any`, `any`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `any`\> + \| `undefined` + +### getPooledQueryIdentity() + +```ts +getPooledQueryIdentity: (query) => string | undefined; +``` + +The identity of a query a partition can serve with no residual conjunct: +its source and its `eq` fields and literals, which determine its rows. +Undefined for any other query, which keeps the full structural identity. + +#### Parameters + +##### query + +[`BaseQueryBuilder`](../classes/BaseQueryBuilder.md) + +#### Returns + +`string` \| `undefined` diff --git a/docs/reference/variables/rightJoin.md b/docs/reference/variables/rightJoin.md new file mode 100644 index 0000000000..d8bb3d1246 --- /dev/null +++ b/docs/reference/variables/rightJoin.md @@ -0,0 +1,34 @@ +--- +id: rightJoin +title: rightJoin +--- + +```ts +const rightJoin: (source, on) => JoinOperator; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:53](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L53) + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) + +### TSource + +`TSource` *extends* [`Source`](../type-aliases/Source.md) + +## Parameters + +### source + +`TSource` + +### on + +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> + +## Returns + +[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `"right"`\> diff --git a/packages/db-collection-e2e/README.md b/packages/db-collection-e2e/README.md index ffb74ddc1e..1bd5a1044b 100644 --- a/packages/db-collection-e2e/README.md +++ b/packages/db-collection-e2e/README.md @@ -330,8 +330,10 @@ it('should join Users and Posts', async () => { const query = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), + .join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), ) .select(({ user, post }) => ({ id: post.id, diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index efae624b6f..ae89ebab41 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -324,19 +324,18 @@ export class BaseQueryBuilder { * // Left join users with posts * query * .from({ users: usersCollection }) - * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) + * .join(leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))) * - * // Inner join with explicit type + * // Inner join * query * .from({ u: usersCollection }) - * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId), 'inner') - * ``` + * .join(innerJoin({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId))) * - * // Join with a subquery - * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active) + * // Join type chosen at run time * query - * .from({ activeUsers }) - * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId)) + * .from({ u: usersCollection }) + * .join(joinOf(type, { p: postsCollection }, ({u, p}) => eq(u.id, p.userId))) + * ``` */ join( operator: JoinOperator, diff --git a/packages/db/src/query/builder/types.ts b/packages/db/src/query/builder/types.ts index 25baa01f70..e280b14c34 100644 --- a/packages/db/src/query/builder/types.ts +++ b/packages/db/src/query/builder/types.ts @@ -1151,7 +1151,7 @@ type ResultValue = TContext[`hasResult`] extends true * from({ users }).where(...) // → User[] * * // Join query without select: - * from({ users }).leftJoin({ orders }, ...) // → { users: User, orders: Order | undefined }[] + * from({ users }).join(leftJoin({ orders }, ...)) // → { users: User, orders: Order | undefined }[] * * // Query with select: * from({ users }).select({ id: users.id, name: users.name }) // → { id: number, name: string }[] @@ -1313,11 +1313,11 @@ export type HasJoinType< * **Example**: * ```typescript * .from({ users }) - * .leftJoin({ orders }, ({ users, orders }) => { + * .join(leftJoin({ orders }, ({ users, orders }) => { * // users is NOT optional here - we can access users.id directly * // orders is NOT optional here - we can access orders.userId directly * return eq(users.id, orders.userId) - * }) + * })) * .where(({ orders }) => { * // NOW orders is optional because it's after the LEFT JOIN * return orders?.status === 'pending' diff --git a/packages/db/src/query/optimizer.ts b/packages/db/src/query/optimizer.ts index f0648b06b7..67783a2134 100644 --- a/packages/db/src/query/optimizer.ts +++ b/packages/db/src/query/optimizer.ts @@ -60,7 +60,7 @@ * ```javascript * query * .from({ users: usersCollection }) - * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id)) + * .join(leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))) * .where(({users}) => eq(users.department_id, 1)) * .where(({posts}) => gt(posts.views, 100)) * .where(({users, posts}) => eq(users.id, posts.author_id)) @@ -74,11 +74,11 @@ * .from({ users: usersCollection }) * .where(({users}) => eq(users.department_id, 1)) * }) - * .join({ + * .join(leftJoin({ * posts: subquery * .from({ posts: postsCollection }) * .where(({posts}) => gt(posts.views, 100)) - * }, ({users, posts}) => eq(users.id, posts.user_id)) + * }, ({users, posts}) => eq(users.id, posts.user_id))) * .where(({users, posts}) => eq(users.id, posts.author_id)) * ``` * @@ -87,7 +87,7 @@ * ```javascript * query * .from({ users: usersCollection }) - * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id)) + * .join(leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))) * .where(({users}) => eq(users.department_id, 1)) * .groupBy(['users.department_id']) * .select({ count: agg('count', '*') }) @@ -101,7 +101,7 @@ * .from({ users: usersCollection }) * .where(({users}) => eq(users.department_id, 1)) * }) - * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id)) + * .join(leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))) * .groupBy(['users.department_id']) * .select({ count: agg('count', '*') }) * ``` diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index 0b357b1d00..745cdba1ac 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -367,9 +367,9 @@ export function warnUnhashableDerivedIdentity( * const { data } = useLiveQuery({ * query: (q) => * q.from({ issues: issueCollection }) - * .join({ persons: personCollection }, ({ issues, persons }) => + * .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) - * ) + * )) * .select(({ issues, persons }) => ({ * id: issues.id, * title: issues.title, diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 3430789030..880bd773e1 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -63,9 +63,9 @@ type InferConditionalResultType = * // Join pattern * const personIssues = useLiveQuery((q) => * q.from({ issues: issueCollection }) - * .join({ persons: personCollection }, ({ issues, persons }) => + * .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) - * ) + * )) * .select(({ issues, persons }) => ({ * id: issues.id, * title: issues.title, diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index ae062cbbac..8dc06b201f 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -177,9 +177,9 @@ function toValue(value: MaybeGetter): T { * // Join pattern * const issuesQuery = useLiveQuery((q) => * q.from({ issues: issueCollection }) - * .join({ persons: personCollection }, ({ issues, persons }) => + * .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) - * ) + * )) * .select(({ issues, persons }) => ({ * id: issues.id, * title: issues.title, diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index e75db9be0e..5eed6eecb0 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -150,9 +150,9 @@ export interface UseLiveQueryReturnWithSingleResultCollection< * // Join pattern * const { data } = useLiveQuery((q) => * q.from({ issues: issueCollection }) - * .join({ persons: personCollection }, ({ issues, persons }) => + * .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) - * ) + * )) * .select(({ issues, persons }) => ({ * id: issues.id, * title: issues.title, diff --git a/scripts/size-compare.mjs b/scripts/size-compare.mjs index 838c79eca0..becc98ca96 100644 --- a/scripts/size-compare.mjs +++ b/scripts/size-compare.mjs @@ -387,22 +387,28 @@ if (runScenarios) { ) let appGzip for (const scenario of scenarios) { - const base = await measureScenario(baseDir, scenario) + // A scenario written for a new API cannot build against an older base. + const base = await measureScenario(baseDir, scenario).catch(() => undefined) const head = await measureScenario(root, scenario) - const baseGzip = base.get(`(bundle)`).gzip + const baseGzip = base?.get(`(bundle)`).gzip const headGzip = head.get(`(bundle)`).gzip appGzip ??= headGzip console.log( [ relative(scenarioDir, scenario).replace(/\.js$/, ``), - baseGzip, + baseGzip ?? `n/a`, headGzip, - signed(headGzip - baseGzip) + percent(headGzip - baseGzip, baseGzip), - signed(head.get(`(bundle)`).brotli - base.get(`(bundle)`).brotli), + baseGzip === undefined + ? `n/a` + : signed(headGzip - baseGzip) + + percent(headGzip - baseGzip, baseGzip), + base + ? signed(head.get(`(bundle)`).brotli - base.get(`(bundle)`).brotli) + : `n/a`, signed(headGzip - appGzip), ].join(`\t`), ) - if (!flag(`files`)) continue + if (!flag(`files`) || !base) continue const changed = [...new Set([...base.keys(), ...head.keys()])] .filter((file) => file !== `(bundle)`) .map((file) => [ diff --git a/scripts/size-scenarios/app-join.js b/scripts/size-scenarios/app-join.js index 89a7ccc676..d27ce8949f 100644 --- a/scripts/size-scenarios/app-join.js +++ b/scripts/size-scenarios/app-join.js @@ -1,5 +1,5 @@ // The typical app, plus a live query that joins a second collection. -import { createCollection, eq } from '@tanstack/db' +import { createCollection, eq, leftJoin } from '@tanstack/db' import { queryCollectionOptions } from '@tanstack/query-db-collection' import { useLiveQuery } from '@tanstack/react-db' import { queryClient, todos } from './app.js' @@ -19,7 +19,9 @@ export function AssignedTodos() { const { data } = useLiveQuery((q) => q .from({ todo: todos }) - .join({ user: users }, ({ todo, user }) => eq(todo.userId, user.id)) + .join( + leftJoin({ user: users }, ({ todo, user }) => eq(todo.userId, user.id)), + ) .select(({ todo, user }) => ({ title: todo.title, owner: user.name })), ) return data From b2abee4b46b14b92b85f879a9b80daec0d6f8463 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 16:35:38 -0600 Subject: [PATCH 08/32] test(db): check that an app bundles only the operators it imports Co-authored-by: Isaac --- .changeset/opt-in-operators.md | 14 +++ docs/contributing/oracle-coverage.md | 19 ++++ .../db/tests/opt-in-operator-bundle.test.ts | 95 +++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 .changeset/opt-in-operators.md create mode 100644 packages/db/tests/opt-in-operator-bundle.test.ts diff --git a/.changeset/opt-in-operators.md b/.changeset/opt-in-operators.md new file mode 100644 index 0000000000..0d35e667be --- /dev/null +++ b/.changeset/opt-in-operators.md @@ -0,0 +1,14 @@ +--- +'@tanstack/db': minor +'@tanstack/react-db': patch +'@tanstack/svelte-db': patch +'@tanstack/vue-db': patch +'@tanstack/solid-db': patch +--- + +Breaking: joins, grouping, includes, and pooling are now opt-in. An app bundles an operator's code only when it imports the value for that operator. A typical app is about 10 kB smaller after gzip. This app has a query collection, a filtered live query, and React `useLiveQuery`. + +- Joins take a join operator. Change `.join(source, on)` to `.join(leftJoin(source, on))`, and `.innerJoin(source, on)` to `.join(innerJoin(source, on))`. `rightJoin`, `fullJoin`, and `joinOf(type, source, on)` work the same way. The old call shape throws `JoinOperatorRequiredError`. +- An include needs `include()`, `toArray()`, or `materialize()`. Change `issues: q.from(...)` in `select()` to `issues: include(q.from(...))`. A subquery in `select()` without one of these throws `IncludeOperatorRequiredError`. +- Aggregates such as `count()` need no change. A `groupBy` with no aggregate takes `groupKeys()`: `.groupBy(groupKeys(({ todo }) => todo.status))`. Without it, the query throws `GroupingOperatorRequiredError`. +- Hooks do not pool live queries by default. To share one partition among many equality-filtered queries, pass `pool: pooledLiveQueries` in the hook's query config: `useLiveQuery({ query, pool: pooledLiveQueries })`. Results do not change. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 5afacbd874..d2b6c98a82 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -3766,3 +3766,22 @@ do not depend on its resolution. Real server ordering, framework unmount scheduling, and published npm behavior require receiving integration witnesses; the present evidence is controlled DB source execution. The revision-bound review record is `docs/contributing/oracle-reviews/issue-2058-paced-serialization.md`. + +## Opt-in operator bundles + +Joins, grouping, includes, and pooling ship only with the value that names +them: a join operator, an aggregate or `groupKeys`, an include function, or +`pooledLiveQueries`. `packages/db/tests/opt-in-operator-bundle.test.ts` owns +that law. It bundles small apps from source with esbuild and reads which +modules reach the output: an app that imports none keeps every operator out, +and each operator's value brings its modules in without the others. The +positive controls keep a renamed or moved module from passing the absence +checks. A mutant that statically imports `BucketFacadeAdapter` into the +live-query builder fails the absence checks. + +The owner checks module reachability only, not byte budgets; `pnpm +size:compare` measures bytes. It bundles `@tanstack/db` alone, with other +packages external, so it does not check db-ivm operators or framework +adapters. Query results under the new API are owned by the existing query and +includes oracles, which run with migrated queries; no oracle compares a +query's old and new spellings on one input. diff --git a/packages/db/tests/opt-in-operator-bundle.test.ts b/packages/db/tests/opt-in-operator-bundle.test.ts new file mode 100644 index 0000000000..2a2b72107f --- /dev/null +++ b/packages/db/tests/opt-in-operator-bundle.test.ts @@ -0,0 +1,95 @@ +// @vitest-environment node +import { fileURLToPath } from 'node:url' +import { build } from 'esbuild' +import { describe, expect, it } from 'vitest' + +/** + * # Does an app pay only for the operators it imports? + * + * Joins, grouping, includes, and pooling are opt-in. Each one's code arrives + * with the value that names it: a join operator (`leftJoin`), an aggregate + * (`count`) or `groupKeys`, an include function (`include`, `toArray`, + * `materialize`), and `pooledLiveQueries`. The law: an app's production + * bundle contains an operator's modules exactly when the app imports one of + * those values. A static import from shared code would break it silently, so + * this bundles small apps from source as an app's build would and reads which + * modules reach the output. + * + * Each operator has a positive control: importing its value must bring its + * modules in. Without the controls, a module renamed or moved would make the + * absence checks pass vacuously. + */ +const index = fileURLToPath(new URL(`../src/index.ts`, import.meta.url)) + +const modules = { + joins: [`src/query/compiler/joins.ts`, `src/query/optimizer-joins.ts`], + grouping: [`src/query/compiler/grouping.ts`], + includes: [ + `src/query/live/bucket-facade-adapter.ts`, + `src/query/live/includes-runtime.ts`, + ], + pooling: [`src/query/pooled-live-query.ts`], +} as const + +/** Bundles an app that exports `names` and returns its modules' output bytes. */ +async function bundledModules(names: Array) { + const result = await build({ + stdin: { + contents: `export { ${names.join(`, `)} } from ${JSON.stringify(index)}`, + resolveDir: fileURLToPath(new URL(`.`, import.meta.url)), + loader: `ts`, + }, + bundle: true, + minify: true, + write: false, + metafile: true, + format: `esm`, + platform: `browser`, + packages: `external`, + tsconfigRaw: `{}`, + logLevel: `error`, + define: { 'process.env.NODE_ENV': `"production"` }, + }) + const output = Object.values(result.metafile.outputs)[0]! + const bytes = new Map() + for (const [input, { bytesInOutput }] of Object.entries(output.inputs)) { + if (bytesInOutput > 0) + bytes.set(input.replace(/^.*?(?=src\/)/, ``), bytesInOutput) + } + return bytes +} + +const typicalApp = [ + `createLiveQueryCollection`, + `eq`, + `and`, + `createCollection`, +] + +describe(`opt-in operator bundles`, () => { + it(`keeps every operator out of an app that imports none`, async () => { + const bytes = await bundledModules(typicalApp) + for (const files of Object.values(modules)) { + for (const file of files) expect(bytes.has(file), file).toBe(false) + } + // Aggregate detection stays in core; the grouping compiler does not. + expect(bytes.get(`src/query/compiler/group-by.ts`) ?? 0).toBeLessThan(1000) + }) + + it.each([ + [`joins`, [`leftJoin`]], + [`grouping`, [`count`]], + [`grouping`, [`groupKeys`]], + [`includes`, [`include`]], + [`includes`, [`toArray`]], + [`pooling`, [`pooledLiveQueries`]], + ] as const)(`brings in %s with %s`, async (operator, names) => { + const bytes = await bundledModules([...typicalApp, ...names]) + for (const file of modules[operator]) + expect(bytes.has(file), file).toBe(true) + for (const [other, files] of Object.entries(modules)) { + if (other === operator) continue + for (const file of files) expect(bytes.has(file), file).toBe(false) + } + }) +}) From 3c5b36976ec93454e867f4131e14395bf0914161 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:44:37 +0000 Subject: [PATCH 09/32] ci: apply automated fixes --- .../src/suites/joins-oracle.suite.ts | 106 +++++++++++++----- .../tests/persisted-readiness-oracle.test.ts | 12 +- packages/db/src/query/builder/functions.ts | 6 +- .../query/live/collection-config-builder.ts | 2 +- packages/db/src/query/pooled-live-query.ts | 5 +- .../db/tests/collection-auto-index.test.ts | 31 +++-- .../collection-cleanup-restart-oracle.test.ts | 20 +++- ...ction-query-publication-boundaries.test.ts | 2 +- ...ction-subscriber-duplicate-inserts.test.ts | 9 +- packages/db/tests/effect.test.ts | 87 ++++++++++---- .../tests/live-query-order-only-move.test.ts | 43 ++++--- packages/db/tests/query/case-when.test-d.ts | 6 +- .../tests/query/functional-variants.test-d.ts | 6 +- packages/db/tests/query/group-by-work.test.ts | 6 +- ...ncludes-collection-oracle.property.test.ts | 6 +- ...includes-functional-input-boundary.test.ts | 6 +- ...ludes-functional-projection-oracle.test.ts | 6 +- .../tests/query/includes-lazy-loading.test.ts | 6 +- .../query/includes-space-oracle-fixture.ts | 6 +- packages/db/tests/query/includes.test-d.ts | 6 +- .../query/pooled-live-query-gc-oracle.test.ts | 5 +- .../tests/query/public-container-copy.test.ts | 6 +- .../query/replay-failure-boundary.test.ts | 6 +- packages/db/tests/query/scheduler.test.ts | 6 +- packages/db/tests/query/union-all.test.ts | 6 +- .../includes-work-counter-oracle.test.ts | 54 +++++---- .../load-subset-lifecycle-oracle.test.ts | 14 ++- .../query-db-collection/tests/query.test.ts | 40 ++++--- .../refetch-ordered-prefix-oracle.test.ts | 13 ++- .../descriptor-query-binding-oracle.test.tsx | 45 +++++--- .../tests/source-id-reuse-oracle.test.tsx | 49 +++++--- packages/react-db/tests/useLiveQuery.test.tsx | 58 ++++++---- packages/solid-db/tests/useLiveQuery.test.tsx | 53 ++++++--- .../tests/useLiveQuery.svelte.test.ts | 34 ++++-- packages/vue-db/tests/useLiveQuery.test.ts | 34 ++++-- 35 files changed, 543 insertions(+), 257 deletions(-) diff --git a/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts b/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts index 91fa8d1f43..582992e8e7 100644 --- a/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts +++ b/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts @@ -1,5 +1,11 @@ import { randomUUID } from 'node:crypto' -import { createLiveQueryCollection, eq, gt, isNull, leftJoin } from '@tanstack/db' +import { + createLiveQueryCollection, + eq, + gt, + isNull, + leftJoin, +} from '@tanstack/db' /** * Joins Test Suite * @@ -382,8 +388,11 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ id: post.id, userName: user.name, @@ -434,8 +443,11 @@ export function createJoinsTestSuite(getConfig: () => Promise) { q .from({ user: usersCollection }) .where(({ user }) => eq(user.isActive, true)) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .where(({ post }) => gt(post.viewCount, 10)) .select(({ user, post }) => ({ id: post.id, @@ -484,8 +496,11 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersEager }) - .join(leftJoin({ post: postsOnDemand }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: postsOnDemand }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ id: post.id, userName: user.name, @@ -533,8 +548,11 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .orderBy(({ post }) => post.viewCount, `desc`) .select(({ user, post }) => ({ id: post.id, @@ -602,8 +620,11 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .orderBy(({ post }) => post.id, `asc`) .limit(10) .offset(5) @@ -649,10 +670,16 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join(leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId))) - .join(leftJoin({ comment: comments }, ({ post, comment }) => - eq(post.id, comment.postId))) + .join( + leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) + .join( + leftJoin({ comment: comments }, ({ post, comment }) => + eq(post.id, comment.postId), + ), + ) .select(({ user, post, comment }) => ({ id: comment.id, userName: user.name, @@ -705,11 +732,17 @@ export function createJoinsTestSuite(getConfig: () => Promise) { q .from({ user: users }) .where(({ user }) => eq(user.isActive, true)) - .join(leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .where(({ post }) => isNull(post.deletedAt)) - .join(leftJoin({ comment: comments }, ({ post, comment }) => - eq(post.id, comment.postId))) + .join( + leftJoin({ comment: comments }, ({ post, comment }) => + eq(post.id, comment.postId), + ), + ) .where(({ comment }) => isNull(comment.deletedAt)) .select(({ user, post, comment }) => ({ id: comment.id, @@ -760,10 +793,16 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersEager }) - .join(leftJoin({ post: postsOnDemand }, ({ user, post }) => - eq(user.id, post.userId))) - .join(leftJoin({ comment: commentsOnDemand }, ({ post, comment }) => - eq(post.id, comment.postId))) + .join( + leftJoin({ post: postsOnDemand }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) + .join( + leftJoin({ comment: commentsOnDemand }, ({ post, comment }) => + eq(post.id, comment.postId), + ), + ) .select(({ user, post, comment }) => ({ id: comment.id, userName: user.name, @@ -811,8 +850,11 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join(leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .where(({ post }) => gt(post.viewCount, 50)) .select(({ user, post }) => ({ id: post.id, @@ -859,8 +901,11 @@ export function createJoinsTestSuite(getConfig: () => Promise) { q .from({ user: users }) .where(({ user }) => gt(user.age, 30)) - .join(leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ id: post.id, userName: user.name, @@ -906,8 +951,11 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join(leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ id: user.id, userName: user.name, diff --git a/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts b/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts index c95a929c08..255f205fb6 100644 --- a/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts +++ b/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts @@ -1,4 +1,5 @@ -import { DbClient, +import { + DbClient, PERSISTED_READINESS, createCollection, createLiveQueryCollection, @@ -6,7 +7,8 @@ import { DbClient, eq, getPersistedReadinessSource, leftJoin, - waitForPersistedInitialRender } from '@tanstack/db' + waitForPersistedInitialRender, +} from '@tanstack/db' import { describe, expect, it, vi } from 'vitest' import { persistedCollectionOptions } from '../src/index.js' import type { PersistenceAdapter } from '../src/index.js' @@ -262,7 +264,11 @@ describe(`persisted-readiness oracle`, () => { query: (q) => q .from({ left }) - .join(leftJoin({ right }, ({ left: a, right: b }) => eq(a.id, b.id))), + .join( + leftJoin({ right }, ({ left: a, right: b }) => + eq(a.id, b.id), + ), + ), }) const observer = createLiveQueryObserver(query) await checkWithCleanup(() => { diff --git a/packages/db/src/query/builder/functions.ts b/packages/db/src/query/builder/functions.ts index 84cf96e487..56610d80bd 100644 --- a/packages/db/src/query/builder/functions.ts +++ b/packages/db/src/query/builder/functions.ts @@ -6,11 +6,7 @@ import { registerWrapper } from './wrapper-identity.js' import type { BasicExpression } from '../ir' import type { IncludesRuntime } from '../live/includes-runtime.js' import type { RefProxy } from './ref-proxy.js' -import type { - Context, - RefLeaf, - StringifiableScalar, -} from './types.js' +import type { Context, RefLeaf, StringifiableScalar } from './types.js' import type { BaseQueryBuilder, QueryBuilder } from './index.js' type StringRef = diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index e9b3dea401..b8d5064ef8 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -28,7 +28,7 @@ import { } from './utils.js' import type { LiveQueryInternalUtils } from './internal.js' import type { IncludesRuntime } from './includes-runtime.js' -import type { BasicExpression, JoinClause, QueryIR } from '../ir.js' +import type { BasicExpression, JoinClause, QueryIR } from '../ir.js' import type { BucketFacadeAdapter } from './bucket-facade-adapter.js' import type { materializeCompilation } from './materialized-pipeline.js' import type { WindowOptions } from '../compiler/index.js' diff --git a/packages/db/src/query/pooled-live-query.ts b/packages/db/src/query/pooled-live-query.ts index 0715bac76d..0cfaa4b321 100644 --- a/packages/db/src/query/pooled-live-query.ts +++ b/packages/db/src/query/pooled-live-query.ts @@ -730,6 +730,9 @@ export function createPooledLiveQuery( * query config to serve equality-filtered queries of one shape from a shared * partition. Apps that do not pass it do not bundle pooling. */ -export const pooledLiveQueries = { createPooledLiveQuery, getPooledQueryIdentity } +export const pooledLiveQueries = { + createPooledLiveQuery, + getPooledQueryIdentity, +} export type PooledLiveQueries = typeof pooledLiveQueries diff --git a/packages/db/tests/collection-auto-index.test.ts b/packages/db/tests/collection-auto-index.test.ts index 39aa43b0fa..1a1b35400b 100644 --- a/packages/db/tests/collection-auto-index.test.ts +++ b/packages/db/tests/collection-auto-index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { createLiveQueryCollection, leftJoin } from '../src' +import { createLiveQueryCollection, leftJoin } from '../src' import { CollectionConfigurationError } from '../src/errors' import { createCollection } from '../src/collection/index.js' import { @@ -582,8 +582,11 @@ describe(`Collection Auto-Indexing`, () => { query: (q: any) => q .from({ item: leftCollection }) - .join(leftJoin({ other: rightCollection }, - ({ item, other }: any) => eq(item.id, other.id2))) + .join( + leftJoin({ other: rightCollection }, ({ item, other }: any) => + eq(item.id, other.id2), + ), + ) .select(({ item, other }: any) => ({ id: item.id, name: item.name, @@ -693,15 +696,19 @@ describe(`Collection Auto-Indexing`, () => { query: (q: any) => q .from({ item: leftCollection }) - .join(leftJoin({ - other: q - .from({ other: rightCollection }) - .select(({ other }: any) => ({ - id2: other.id2, - name: other.name, - })), - }, - ({ item, other }: any) => eq(item.id, other.id2))) + .join( + leftJoin( + { + other: q + .from({ other: rightCollection }) + .select(({ other }: any) => ({ + id2: other.id2, + name: other.name, + })), + }, + ({ item, other }: any) => eq(item.id, other.id2), + ), + ) .select(({ item, other }: any) => ({ id: item.id, name: item.name, diff --git a/packages/db/tests/collection-cleanup-restart-oracle.test.ts b/packages/db/tests/collection-cleanup-restart-oracle.test.ts index 93a115977f..7730b9aaa7 100644 --- a/packages/db/tests/collection-cleanup-restart-oracle.test.ts +++ b/packages/db/tests/collection-cleanup-restart-oracle.test.ts @@ -1,11 +1,13 @@ import { describe, expect, it, vi } from 'vitest' -import { BasicIndex, +import { + BasicIndex, createCollection, createEffect, createLiveQueryCollection, eq, innerJoin, - withCollectionSyncConfigCleanup } from '../src' + withCollectionSyncConfigCleanup, +} from '../src' import { createDeferred } from '../src/deferred' import type { SyncConfig } from '../src/types' @@ -519,8 +521,11 @@ describe(`Collection cleanup admission oracle`, () => { const live = createLiveQueryCollection((q) => q .from({ left: source }) - .join(innerJoin({ right: source }, - ({ left, right }) => eq(left.id, right.id))), + .join( + innerJoin({ right: source }, ({ left, right }) => + eq(left.id, right.id), + ), + ), ) const reports = vi.spyOn(console, `error`).mockImplementation(() => {}) @@ -878,8 +883,11 @@ describe(`Collection cleanup admission oracle`, () => { live = createLiveQueryCollection((q) => q .from({ first: firstSource }) - .join(innerJoin({ second: secondSource }, - ({ first, second }) => eq(first.id, second.id))), + .join( + innerJoin({ second: secondSource }, ({ first, second }) => + eq(first.id, second.id), + ), + ), ) const message = `Source collection '${cleaningSource.id}' was manually cleaned up while live query '${live.id}' depends on it. ` + diff --git a/packages/db/tests/collection-query-publication-boundaries.test.ts b/packages/db/tests/collection-query-publication-boundaries.test.ts index f988752d2f..868c46bcbd 100644 --- a/packages/db/tests/collection-query-publication-boundaries.test.ts +++ b/packages/db/tests/collection-query-publication-boundaries.test.ts @@ -1,6 +1,6 @@ import { expect, it } from 'vitest' import { MultiSet } from '@tanstack/db-ivm' -import { createLiveQueryCollection, eq, innerJoin } from '../src/query/index.js' +import { createLiveQueryCollection, eq, innerJoin } from '../src/query/index.js' import { createCollection } from '../src/collection/index.js' import { createDeferred } from '../src/deferred.js' import { createTransaction } from '../src/transactions.js' diff --git a/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts b/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts index 18ede5b64a..64b2ff0ee8 100644 --- a/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts +++ b/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { createLiveQueryCollection, eq, leftJoin } from '../src/query/index.js' +import { createLiveQueryCollection, eq, leftJoin } from '../src/query/index.js' import { createCollection } from '../src/collection/index.js' import { BTreeIndex } from '../src/indexes/btree-index.js' import { mockSyncCollectionOptions } from './utils.js' @@ -505,8 +505,11 @@ describe(`CollectionSubscriber duplicate insert prevention`, () => { const liveQueryCollection = createLiveQueryCollection((q) => q .from({ users: usersCollection }) - .join(leftJoin({ orders: ordersCollection }, ({ users: u, orders: o }) => - eq(u.id, o.userId))) + .join( + leftJoin({ orders: ordersCollection }, ({ users: u, orders: o }) => + eq(u.id, o.userId), + ), + ) .select(({ users: u2, orders: o2 }) => ({ orderId: o2.id, userName: u2.name, diff --git a/packages/db/tests/effect.test.ts b/packages/db/tests/effect.test.ts index 4bd2f08be4..d91708d839 100644 --- a/packages/db/tests/effect.test.ts +++ b/packages/db/tests/effect.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it, vi } from 'vitest' -import { Query, createEffect, createTransaction, eq, leftJoin } from '../src/index.js' +import { + Query, + createEffect, + createTransaction, + eq, + leftJoin, +} from '../src/index.js' import { createCollection } from '../src/collection/index.js' import { BTreeIndex } from '../src/indexes/btree-index.js' import { @@ -919,8 +925,11 @@ describe(`createEffect`, () => { query: (q) => q .from({ issue: issues }) - .join(leftJoin({ user: users }, ({ issue, user }) => - eq(issue.userId, user.id))) + .join( + leftJoin({ user: users }, ({ issue, user }) => + eq(issue.userId, user.id), + ), + ) .select(({ issue, user }) => ({ issueId: issue.id, title: issue.title, @@ -1665,8 +1674,13 @@ describe(`createEffect`, () => { return q .from({ left: leftTop }) - .join(leftJoin({ right: rightTop }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.id, rightRow.id))) + .join( + leftJoin( + { right: rightTop }, + ({ left: leftRow, right: rightRow }) => + eq(leftRow.id, rightRow.id), + ), + ) .select(({ left: leftRow, right: rightRow }) => ({ id: leftRow.id, leftName: leftRow.name, @@ -1720,8 +1734,11 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join(leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId))) + .join( + leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), + ), + ) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -1762,8 +1779,11 @@ describe(`createEffect`, () => { query: (q) => q .from({ left }) - .join(leftJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.id, rightRow.id))), + .join( + leftJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.id, rightRow.id), + ), + ), onBatch: () => {}, }) @@ -1824,8 +1844,11 @@ describe(`createEffect`, () => { query: (q) => q .from({ left }) - .join(leftJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.id, rightRow.id))), + .join( + leftJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.id, rightRow.id), + ), + ), onBatch: () => {}, }), ).toThrow(startupFailure) @@ -1934,8 +1957,11 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join(leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId))) + .join( + leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), + ), + ) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -1980,8 +2006,11 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join(leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId))) + .join( + leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), + ), + ) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2050,8 +2079,11 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join(leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId))) + .join( + leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), + ), + ) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2219,8 +2251,11 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join(leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId))) + .join( + leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), + ), + ) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2275,8 +2310,11 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join(leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId))) + .join( + leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), + ), + ) .select(({ user, issue }) => ({ id: user.id, issueId: issue.id, @@ -2475,8 +2513,11 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join(leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId))) + .join( + leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), + ), + ) .fn.select(({ user }) => { if (phase === `projection`) disposeInTurn?.() return user diff --git a/packages/db/tests/live-query-order-only-move.test.ts b/packages/db/tests/live-query-order-only-move.test.ts index da333ea550..25617c1821 100644 --- a/packages/db/tests/live-query-order-only-move.test.ts +++ b/packages/db/tests/live-query-order-only-move.test.ts @@ -393,11 +393,13 @@ describe(`order-only move (RFC #1623 phase 4)`, () => { const lq = createLiveQueryCollection((q) => q.from({ parent: parents }).select(({ parent }) => ({ id: parent.id, - children: include(q - .from({ child: children }) - .where(({ child }) => eq(child.parentId, parent.id)) - .orderBy(({ child }) => child.position) - .select(({ child }) => ({ id: child.id, name: child.name }))), + children: include( + q + .from({ child: children }) + .where(({ child }) => eq(child.parentId, parent.id)) + .orderBy(({ child }) => child.position) + .select(({ child }) => ({ id: child.id, name: child.name })), + ), })), ) await lq.preload() @@ -464,18 +466,25 @@ describe(`order-only move (RFC #1623 phase 4)`, () => { const lq = createLiveQueryCollection((q) => q.from({ org: orgs }).select(({ org }) => ({ id: org.id, - teams: include(q - .from({ team: teams }) - .where(({ team }) => eq(team.orgId, org.id)) - .orderBy(({ team }) => team.position) - .select(({ team }) => ({ - id: team.id, - members: include(q - .from({ member: members }) - .where(({ member }) => eq(member.teamId, team.id)) - .orderBy(({ member }) => member.position) - .select(({ member }) => ({ id: member.id, name: member.name }))), - }))), + teams: include( + q + .from({ team: teams }) + .where(({ team }) => eq(team.orgId, org.id)) + .orderBy(({ team }) => team.position) + .select(({ team }) => ({ + id: team.id, + members: include( + q + .from({ member: members }) + .where(({ member }) => eq(member.teamId, team.id)) + .orderBy(({ member }) => member.position) + .select(({ member }) => ({ + id: member.id, + name: member.name, + })), + ), + })), + ), })), ) await lq.preload() diff --git a/packages/db/tests/query/case-when.test-d.ts b/packages/db/tests/query/case-when.test-d.ts index 5aa3b0d008..246e07a164 100644 --- a/packages/db/tests/query/case-when.test-d.ts +++ b/packages/db/tests/query/case-when.test-d.ts @@ -1,11 +1,13 @@ import { describe, expectTypeOf, test } from 'vitest' -import { caseWhen, +import { + caseWhen, createLiveQueryCollection, eq, gt, include, leftJoin, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' diff --git a/packages/db/tests/query/functional-variants.test-d.ts b/packages/db/tests/query/functional-variants.test-d.ts index 796c494d4e..207c83c1f4 100644 --- a/packages/db/tests/query/functional-variants.test-d.ts +++ b/packages/db/tests/query/functional-variants.test-d.ts @@ -1,5 +1,6 @@ import { describe, expectTypeOf, test } from 'vitest' -import { Query, +import { + Query, caseWhen, count, createLiveQueryCollection, @@ -8,7 +9,8 @@ import { Query, include, leftJoin, materialize, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' diff --git a/packages/db/tests/query/group-by-work.test.ts b/packages/db/tests/query/group-by-work.test.ts index 148025bd0f..bb5fcc1f10 100644 --- a/packages/db/tests/query/group-by-work.test.ts +++ b/packages/db/tests/query/group-by-work.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it, vi } from 'vitest' -import { createLiveQueryCollection, eq, include } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + include, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { count, max, sum } from '../../src/query/builder/functions.js' import { mockSyncCollectionOptions } from '../utils.js' diff --git a/packages/db/tests/query/includes-collection-oracle.property.test.ts b/packages/db/tests/query/includes-collection-oracle.property.test.ts index b3aafd89e1..83dbe35226 100644 --- a/packages/db/tests/query/includes-collection-oracle.property.test.ts +++ b/packages/db/tests/query/includes-collection-oracle.property.test.ts @@ -1,6 +1,7 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect } from 'vitest' -import { add, +import { + add, caseWhen, concat, count, @@ -14,7 +15,8 @@ import { add, materialize, multiply, sum, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { runTrace } from '../trace-runner.js' diff --git a/packages/db/tests/query/includes-functional-input-boundary.test.ts b/packages/db/tests/query/includes-functional-input-boundary.test.ts index 9036a2fe57..222ed5d824 100644 --- a/packages/db/tests/query/includes-functional-input-boundary.test.ts +++ b/packages/db/tests/query/includes-functional-input-boundary.test.ts @@ -1,9 +1,11 @@ import { describe, expect, it } from 'vitest' -import { createLiveQueryCollection, +import { + createLiveQueryCollection, eq, include, materialize, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createControlledCollection } from './includes-oracle-helpers.js' describe(`functional include input boundary`, () => { diff --git a/packages/db/tests/query/includes-functional-projection-oracle.test.ts b/packages/db/tests/query/includes-functional-projection-oracle.test.ts index 7ba45e2be6..fb3d280055 100644 --- a/packages/db/tests/query/includes-functional-projection-oracle.test.ts +++ b/packages/db/tests/query/includes-functional-projection-oracle.test.ts @@ -1,9 +1,11 @@ import { describe, expect, it, vi } from 'vitest' -import { createLiveQueryCollection, +import { + createLiveQueryCollection, eq, include, materialize, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { BasicIndex } from '../../src/indexes/basic-index.js' diff --git a/packages/db/tests/query/includes-lazy-loading.test.ts b/packages/db/tests/query/includes-lazy-loading.test.ts index c0b7ce2f94..708d7bb977 100644 --- a/packages/db/tests/query/includes-lazy-loading.test.ts +++ b/packages/db/tests/query/includes-lazy-loading.test.ts @@ -1,11 +1,13 @@ import { describe, expect, it, vi } from 'vitest' -import { and, +import { + and, createLiveQueryCollection, eq, gte, include, leftJoin, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { extractSimpleComparisons } from '../../src/query/expression-helpers.js' import { diff --git a/packages/db/tests/query/includes-space-oracle-fixture.ts b/packages/db/tests/query/includes-space-oracle-fixture.ts index a3910efa91..059cd45c31 100644 --- a/packages/db/tests/query/includes-space-oracle-fixture.ts +++ b/packages/db/tests/query/includes-space-oracle-fixture.ts @@ -1,4 +1,8 @@ -import { createLiveQueryCollection, eq, include } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + include, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { localOnlyCollectionOptions } from '../../src/local-only.js' diff --git a/packages/db/tests/query/includes.test-d.ts b/packages/db/tests/query/includes.test-d.ts index 0a79ad5460..7f0aadbfe6 100644 --- a/packages/db/tests/query/includes.test-d.ts +++ b/packages/db/tests/query/includes.test-d.ts @@ -1,12 +1,14 @@ import { describe, expectTypeOf, test } from 'vitest' -import { Query, +import { + Query, concat, createLiveQueryCollection, eq, include, materialize, queryOnce, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { WithVirtualProps } from '../../src/virtual-props.js' diff --git a/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts b/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts index aeb7506588..2ed7d27e55 100644 --- a/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts +++ b/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts @@ -1,5 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createPooledLiveQuery, pooledLiveQueries } from '../../src/query/pooled-live-query.js' +import { + createPooledLiveQuery, + pooledLiveQueries, +} from '../../src/query/pooled-live-query.js' import { CollectionImpl, createCollection } from '../../src/collection/index.js' import { createLiveQueryObserver } from '../../src/live-query-observer.js' import { Query } from '../../src/query/builder/index.js' diff --git a/packages/db/tests/query/public-container-copy.test.ts b/packages/db/tests/query/public-container-copy.test.ts index a6571aab0a..c4d75776e9 100644 --- a/packages/db/tests/query/public-container-copy.test.ts +++ b/packages/db/tests/query/public-container-copy.test.ts @@ -1,9 +1,11 @@ import { expect, it } from 'vitest' -import { createLiveQueryCollection, +import { + createLiveQueryCollection, eq, include, materialize, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { transformPublicContainers } from '../../src/query/compiler/route-metadata.js' import { createControlledCollection } from './includes-oracle-helpers.js' diff --git a/packages/db/tests/query/replay-failure-boundary.test.ts b/packages/db/tests/query/replay-failure-boundary.test.ts index fa2018640b..11022d1e6c 100644 --- a/packages/db/tests/query/replay-failure-boundary.test.ts +++ b/packages/db/tests/query/replay-failure-boundary.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' -import { createLiveQueryCollection, eq, include } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + include, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection' import { createDeferred } from '../../src/deferred' import { BasicIndex } from '../../src/indexes/basic-index' diff --git a/packages/db/tests/query/scheduler.test.ts b/packages/db/tests/query/scheduler.test.ts index 4fa8423d5f..97c0079b00 100644 --- a/packages/db/tests/query/scheduler.test.ts +++ b/packages/db/tests/query/scheduler.test.ts @@ -1,11 +1,13 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { createLiveQueryCollection, +import { + createLiveQueryCollection, eq, fullJoin, include, innerJoin, isNull, - leftJoin } from '../../src/query/index.js' + leftJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { createTransaction } from '../../src/transactions.js' diff --git a/packages/db/tests/query/union-all.test.ts b/packages/db/tests/query/union-all.test.ts index 1ca1ff6bb0..2f2d37c557 100644 --- a/packages/db/tests/query/union-all.test.ts +++ b/packages/db/tests/query/union-all.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import { caseWhen, +import { + caseWhen, coalesce, concat, count, @@ -11,7 +12,8 @@ import { caseWhen, innerJoin, leftJoin, rightJoin, - toArray } from '../../src/query/index.js' + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { diff --git a/packages/query-db-collection/tests/includes-work-counter-oracle.test.ts b/packages/query-db-collection/tests/includes-work-counter-oracle.test.ts index 799aeaa260..0055b1b4fa 100644 --- a/packages/query-db-collection/tests/includes-work-counter-oracle.test.ts +++ b/packages/query-db-collection/tests/includes-work-counter-oracle.test.ts @@ -1,8 +1,10 @@ -import { BasicIndex, +import { + BasicIndex, createCollection, createLiveQueryCollection, eq, - include } from '@tanstack/db' + include, +} from '@tanstack/db' import { QueryClient } from '@tanstack/query-core' import { describe, expect, it } from 'vitest' import { evaluateReferenceExpression } from '../../db/tests/reference-expression-oracle' @@ -362,27 +364,33 @@ async function checkNestedTreeShape( q.from({ root: sources.roots }).select(({ root }) => ({ id: root.id, value: root.value, - children: include(q - .from({ branch: sources.branches }) - .where(({ branch }) => eq(branch.parentId, root.id)) - .select(({ branch }) => ({ - id: branch.id, - value: branch.value, - children: include(q - .from({ twig: sources.twigs }) - .where(({ twig }) => eq(twig.parentId, branch.id)) - .select(({ twig }) => ({ - id: twig.id, - value: twig.value, - children: include(q - .from({ leaf: sources.leaves }) - .where(({ leaf }) => eq(leaf.parentId, twig.id)) - .select(({ leaf }) => ({ - id: leaf.id, - value: leaf.value, - }))), - }))), - }))), + children: include( + q + .from({ branch: sources.branches }) + .where(({ branch }) => eq(branch.parentId, root.id)) + .select(({ branch }) => ({ + id: branch.id, + value: branch.value, + children: include( + q + .from({ twig: sources.twigs }) + .where(({ twig }) => eq(twig.parentId, branch.id)) + .select(({ twig }) => ({ + id: twig.id, + value: twig.value, + children: include( + q + .from({ leaf: sources.leaves }) + .where(({ leaf }) => eq(leaf.parentId, twig.id)) + .select(({ leaf }) => ({ + id: leaf.id, + value: leaf.value, + })), + ), + })), + ), + })), + ), })), }) rootCollection = roots diff --git a/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts b/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts index c91b62dd57..470cb66895 100644 --- a/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts +++ b/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts @@ -1,10 +1,12 @@ -import { BasicIndex, +import { + BasicIndex, IR, createCollection, createLiveQueryCollection, eq, getLoadSubsetDemandKey, - leftJoin } from '@tanstack/db' + leftJoin, +} from '@tanstack/db' import { QueryClient } from '@tanstack/query-core' import { describe, expect, it, vi } from 'vitest' import { TraceAssertionError } from '../../db/tests/trace-runner.js' @@ -1300,8 +1302,12 @@ async function expectEveryFailedSourceToRecover(): Promise { const live = createLiveQueryCollection((query) => query .from({ left: left.collection }) - .join(leftJoin({ right: right.collection }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.id, rightRow.id))) + .join( + leftJoin( + { right: right.collection }, + ({ left: leftRow, right: rightRow }) => eq(leftRow.id, rightRow.id), + ), + ) .select(({ left: row }) => ({ id: row.id })), ) const preload = live.preload() diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index e10ce924df..a9575b0347 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -1,4 +1,5 @@ -import { BTreeIndex, +import { + BTreeIndex, DbClient, IR, collectionOptions, @@ -10,7 +11,8 @@ import { BTreeIndex, inArray, include, or, - resetWarnings } from '@tanstack/db' + resetWarnings, +} from '@tanstack/db' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { QueryClient, @@ -1384,14 +1386,16 @@ describe(`QueryCollection`, () => { const collection = createLiveQueryCollection((q) => q.from({ lineItem: lineItems }).select(({ lineItem }) => ({ id: lineItem.id, - product: include(q - .from({ product: products }) - .where(({ product }) => eq(product.id, lineItem.productId)) - .select(({ product }) => ({ - id: product.id, - categoryId: product.categoryId, - name: product.name, - }))), + product: include( + q + .from({ product: products }) + .where(({ product }) => eq(product.id, lineItem.productId)) + .select(({ product }) => ({ + id: product.id, + categoryId: product.categoryId, + name: product.name, + })), + ), })), ) @@ -1464,13 +1468,15 @@ describe(`QueryCollection`, () => { const live = createLiveQueryCollection((q) => q.from({ lineItem: lineItems }).select(({ lineItem }) => ({ id: lineItem.id, - product: include(q - .from({ product: products }) - .where(({ product }) => eq(product.id, lineItem.productId)) - .select(({ product }) => ({ - id: product.id, - name: product.name, - }))), + product: include( + q + .from({ product: products }) + .where(({ product }) => eq(product.id, lineItem.productId)) + .select(({ product }) => ({ + id: product.id, + name: product.name, + })), + ), })), ) diff --git a/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts b/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts index 4f5090247d..b427bca2b5 100644 --- a/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts +++ b/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts @@ -1,4 +1,5 @@ -import { BTreeIndex, +import { + BTreeIndex, IR, createCollection, createLiveQueryCollection, @@ -6,7 +7,8 @@ import { BTreeIndex, getLoadSubsetDemandKey, isUndefined, leftJoin, - not } from '@tanstack/db' + not, +} from '@tanstack/db' import { QueryClient } from '@tanstack/query-core' import fc from 'fast-check' import { describe, expect, it, vi } from 'vitest' @@ -638,8 +640,11 @@ async function startRun(testCase: Case): Promise { const window = createLiveQueryCollection((q) => { const ordered = q .from({ item: items }) - .join(leftJoin({ marker: markerCollection }, ({ item, marker }) => - eq(item.id, marker.itemId))) + .join( + leftJoin({ marker: markerCollection }, ({ item, marker }) => + eq(item.id, marker.itemId), + ), + ) .where(({ marker }) => not(isUndefined(marker.itemId))) .orderBy( ({ item }) => (scenario.orderKey === `rank` ? item.rank : item.label), diff --git a/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx b/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx index de6d6ca312..167437846c 100644 --- a/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx +++ b/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx @@ -1,4 +1,7 @@ -import { DbClient, Query, collectionOptions, +import { + DbClient, + Query, + collectionOptions, createCollection, eq, getStableQueryBuilderHash, @@ -7,7 +10,8 @@ import { DbClient, Query, collectionOptions, joinOf, leftJoin, prepareLiveQueryValue, - toArray } from '@tanstack/db' + toArray, +} from '@tanstack/db' /** * A Query assembled with a Collection descriptor is a reusable query plan. * Its source Collection belongs to the DbClient that later consumes it, so @@ -186,7 +190,11 @@ describe(`standalone descriptor query binding`, () => { firstRenderDeferrals.clear() const continued = prepared - .join(leftJoin({ late: lateDescriptor }, ({ base, late }) => eq(base.id, late.id))) + .join( + leftJoin({ late: lateDescriptor }, ({ base, late }) => + eq(base.id, late.id), + ), + ) .select(({ base, late }) => ({ id: base.id, value: late.value })) expect(lateStarts).toBe(0) @@ -234,8 +242,11 @@ describe(`standalone descriptor query binding`, () => { client, deferrals, ) as typeof original - prepared.join(leftJoin({ late: lateDescriptor }, ({ base, late }) => - eq(base.id, late.id))) + prepared.join( + leftJoin({ late: lateDescriptor }, ({ base, late }) => + eq(base.id, late.id), + ), + ) throw never } @@ -855,8 +866,11 @@ describe(`standalone descriptor query binding`, () => { const joined = () => new Query() .from({ parent: parentDescriptor }) - .join(innerJoin({ child: childDescriptor }, ({ parent, child }) => - eq(parent.id, child.parentId))) + .join( + innerJoin({ child: childDescriptor }, ({ parent, child }) => + eq(parent.id, child.parentId), + ), + ) .select(({ parent, child }) => ({ id: parent.id, value: parent.value, @@ -1340,12 +1354,17 @@ describe(`standalone descriptor query binding`, () => { : anchors const child = new Query() .from({ [alias]: mains }) - .join(joinOf(kind, { anchor: joinedSource }, - (context: Record) => - eq( - (context[alias] as Main).anchorId, - (context.anchor as Anchor).id, - ))) + .join( + joinOf( + kind, + { anchor: joinedSource }, + (context: Record) => + eq( + (context[alias] as Main).anchorId, + (context.anchor as Anchor).id, + ), + ), + ) .where((context: Record) => correlation === `joined` ? eq((context.anchor as Anchor).parentId, parent.id) diff --git a/packages/react-db/tests/source-id-reuse-oracle.test.tsx b/packages/react-db/tests/source-id-reuse-oracle.test.tsx index 77b2b3685f..20769f9459 100644 --- a/packages/react-db/tests/source-id-reuse-oracle.test.tsx +++ b/packages/react-db/tests/source-id-reuse-oracle.test.tsx @@ -1,9 +1,11 @@ -import { DbClient, +import { + DbClient, collectionOptions, createCollection, eq, gt, - leftJoin } from '@tanstack/db' + leftJoin, +} from '@tanstack/db' /** * Source ID reuse contract for React's derived live-query identity. * @@ -149,12 +151,22 @@ describe(`React source ID reuse`, () => { include ? q .from({ failingA: failing[0]! }) - .join(leftJoin({ failingB: failing[1]! }, ({ failingA, failingB }) => - eq(failingA.id, failingB.id))) - .join(leftJoin({ healthy }, ({ failingA, healthy }) => - eq(failingA.id, healthy.id))) - .join(leftJoin({ settings: current }, ({ failingA, settings }) => - eq(failingA.id, settings.id))) + .join( + leftJoin( + { failingB: failing[1]! }, + ({ failingA, failingB }) => eq(failingA.id, failingB.id), + ), + ) + .join( + leftJoin({ healthy }, ({ failingA, healthy }) => + eq(failingA.id, healthy.id), + ), + ) + .join( + leftJoin({ settings: current }, ({ failingA, settings }) => + eq(failingA.id, settings.id), + ), + ) : q.from({ settings: current }), }) return
Query
@@ -222,8 +234,11 @@ describe(`React source ID reuse`, () => { includeShared ? q .from({ settings: current }) - .join(leftJoin({ shared: descriptor }, ({ settings, shared }) => - eq(settings.id, shared.id))) + .join( + leftJoin({ shared: descriptor }, ({ settings, shared }) => + eq(settings.id, shared.id), + ), + ) : q.from({ settings: current }), }) return
Query
@@ -334,8 +349,11 @@ describe(`React source ID reuse`, () => { query: (q) => q .from({ first: leftSource }) - .join(leftJoin({ second: rightSource }, ({ first, second }) => - eq(first.id, second.id))), + .join( + leftJoin({ second: rightSource }, ({ first, second }) => + eq(first.id, second.id), + ), + ), }), ), ).toThrow(/source Collection .*same ID/i) @@ -720,8 +738,11 @@ describe(`React source ID reuse: useLiveInfiniteQuery`, () => { includeShared ? q .from({ settings: current }) - .join(leftJoin({ shared: descriptor }, ({ settings, shared }) => - eq(settings.id, shared.id))) + .join( + leftJoin({ shared: descriptor }, ({ settings, shared }) => + eq(settings.id, shared.id), + ), + ) .orderBy(({ settings }) => settings.rank) : q .from({ settings: current }) diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index c4cde7d8e4..c7f9fd1aeb 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -1,4 +1,6 @@ -import { DbClient, Query , +import { + DbClient, + Query, coalesce, collectionOptions, count, @@ -12,7 +14,8 @@ import { DbClient, Query , localOnlyCollectionOptions, lte, sum, - toArray } from '@tanstack/db' + toArray, +} from '@tanstack/db' import { describe, expect, it, vi } from 'vitest' import { act, renderHook, waitFor } from '@testing-library/react' import { useEffect } from 'react' @@ -446,8 +449,11 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -820,8 +826,11 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1457,8 +1466,11 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1882,8 +1894,11 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -2635,8 +2650,11 @@ describe(`Query Collections`, () => { return q .from({ persons: personCollection }) - .join(leftJoin({ ic: issueCountSubquery }, ({ persons, ic }) => - eq(persons.id, ic.userId))) + .join( + leftJoin({ ic: issueCountSubquery }, ({ persons, ic }) => + eq(persons.id, ic.userId), + ), + ) .select(({ persons, ic }) => ({ name: persons.name, issueCount: ic.issueCount, @@ -2755,13 +2773,15 @@ describe(`Query Collections`, () => { q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: include(q - .from({ i: issuesCollection }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - }))), + issues: include( + q + .from({ i: issuesCollection }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), })), ) }) diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index 37792338f6..f7aac8db1e 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -1,4 +1,6 @@ -import { Query, count , +import { + Query, + count, createCollection, createLiveQueryCollection, createOptimisticAction, @@ -7,7 +9,8 @@ import { Query, count , include, leftJoin, toArray, - waitForPersistedInitialRender } from '@tanstack/db' + waitForPersistedInitialRender, +} from '@tanstack/db' import { describe, expect, it, vi } from 'vitest' import { render, renderHook, waitFor } from '@solidjs/testing-library' import { @@ -381,8 +384,11 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -864,8 +870,11 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1437,8 +1446,11 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1834,8 +1846,11 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -3463,14 +3478,16 @@ describe(`includes subqueries`, () => { useLiveQuery((q) => q.from({ project: projects }).select(({ project }) => ({ id: project.id, - issues: include(q - .from({ issue: issues }) - .where(({ issue }) => eq(issue.projectId, project.id)) - .select(({ issue }) => ({ - id: issue.id, - projectId: issue.projectId, - title: issue.title, - }))), + issues: include( + q + .from({ issue: issues }) + .where(({ issue }) => eq(issue.projectId, project.id)) + .select(({ issue }) => ({ + id: issue.id, + projectId: issue.projectId, + title: issue.title, + })), + ), })), ), ) diff --git a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts index fc6ef847f9..91d6dc03aa 100644 --- a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts +++ b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts @@ -1,4 +1,5 @@ -import { BaseQueryBuilder, +import { + BaseQueryBuilder, DbClient, count, createCollection, @@ -7,7 +8,8 @@ import { BaseQueryBuilder, getLiveQueryHash, getStableValueHash, gt, - leftJoin } from '@tanstack/db' + leftJoin, +} from '@tanstack/db' import { afterEach, describe, expect, it } from 'vitest' import { flushSync } from 'svelte' import { useLiveQuery } from '../src/useLiveQuery.svelte.js' @@ -488,8 +490,11 @@ describe(`Query Collections`, () => { const query = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -773,8 +778,11 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1287,8 +1295,11 @@ describe(`Query Collections`, () => { const query = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1838,8 +1849,11 @@ describe(`Query Collections`, () => { const query = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index d3e2755e0b..472490969a 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -1,10 +1,12 @@ -import { count, +import { + count, createCollection, createLiveQueryCollection, createOptimisticAction, eq, gt, - leftJoin } from '@tanstack/db' + leftJoin, +} from '@tanstack/db' import { describe, expect, it } from 'vitest' import { nextTick, ref, watchEffect } from 'vue' import { useLiveQuery } from '../src/useLiveQuery' @@ -315,8 +317,11 @@ describe(`Query Collections`, () => { const { state } = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -603,8 +608,11 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1174,8 +1182,11 @@ describe(`Query Collections`, () => { const { isReady } = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, @@ -1700,8 +1711,11 @@ describe(`Query Collections`, () => { const { isLoading, state, data } = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id))) + .join( + leftJoin({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), + ), + ) .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, From e1e0297adb309715e59f01813df57fc855a96507 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 21:23:28 -0600 Subject: [PATCH 10/32] fix(db): keep pool out of DbClient query identity; let Angular params pool A DbClient query never pools, so prepareLiveQueryValue drops pool and a server preload hashes like a pooling hook. pool moves from LiveQueryCollectionConfig to LiveQueryHookConfig, which the framework hooks accept. Angular's { params, query } form takes pool. Join operators have a named function type so their declarations stay portable. The benchmark runs against either join API, and the minified API check lists the new errors. Co-authored-by: Isaac --- packages/angular-db/src/index.ts | 16 +++- .../tests/inject-live-query.test.ts | 43 +++++++++++ packages/db/src/live-query-options.ts | 15 +++- .../db/src/query/builder/join-operators.ts | 23 ++++-- packages/db/src/query/index.ts | 5 +- packages/db/src/query/live/types.ts | 20 +++-- packages/db/tests/live-query-options.test.ts | 16 ++++ packages/react-db/src/useLiveQuery.ts | 6 ++ packages/solid-db/src/useLiveQuery.ts | 4 +- packages/svelte-db/src/useLiveQuery.svelte.ts | 4 +- packages/vue-db/src/useLiveQuery.ts | 4 +- scripts/bench/incremental-update.ts | 73 ++++++++++++------- scripts/test-minified-db.mjs | 4 +- 13 files changed, 177 insertions(+), 56 deletions(-) diff --git a/packages/angular-db/src/index.ts b/packages/angular-db/src/index.ts index 4f6b0f1c6d..ff1af05f94 100644 --- a/packages/angular-db/src/index.ts +++ b/packages/angular-db/src/index.ts @@ -21,10 +21,11 @@ import type { GetResult, InferResultType, InitialQueryBuilder, - LiveQueryCollectionConfig, + LiveQueryHookConfig, LiveQueryObserver, LiveQueryPersistedStatus, NonSingleResult, + PooledLiveQueries, QueryBuilder, SingleResult, } from '@tanstack/db' @@ -121,6 +122,8 @@ export function injectLiveQuery< TParams extends any, >(options: { params: () => TParams + /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */ + pool?: PooledLiveQueries query: (args: { params: TParams q: InitialQueryBuilder @@ -131,6 +134,8 @@ export function injectLiveQuery< TParams extends any, >(options: { params: () => TParams + /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */ + pool?: PooledLiveQueries query: (args: { params: TParams q: InitialQueryBuilder @@ -145,7 +150,7 @@ export function injectLiveQuery( ) => QueryBuilder | undefined | null, ): InjectConditionalLiveQueryResult export function injectLiveQuery( - config: LiveQueryCollectionConfig, + config: LiveQueryHookConfig, ): InjectLiveQueryResult // Pre-created collection without singleResult export function injectLiveQuery< @@ -194,7 +199,7 @@ export function injectLiveQuery(opts: any) { typeof opts.params === `function` if (isReactiveQueryOptions) { - const { params, query } = opts + const { params, query, pool } = opts const currentParams = params() // Check if query function returns null/undefined (disabled query) @@ -206,7 +211,10 @@ export function injectLiveQuery(opts: any) { return null } - return resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS }) + return resolveLiveQueryValue(result, { + gcTime: DEFAULT_GC_TIME_MS, + pool, + }) } // Handle LiveQueryCollectionConfig objects. Default startSync/gcTime to diff --git a/packages/angular-db/tests/inject-live-query.test.ts b/packages/angular-db/tests/inject-live-query.test.ts index 1c90a76025..d8343c6c92 100644 --- a/packages/angular-db/tests/inject-live-query.test.ts +++ b/packages/angular-db/tests/inject-live-query.test.ts @@ -6,6 +6,7 @@ import { createLiveQueryCollection, eq, gt, + pooledLiveQueries, } from '@tanstack/db' import { injectLiveQuery } from '../src/index' import { mockSyncCollectionOptions } from '../../db/tests/utils' @@ -613,6 +614,48 @@ describe(`injectLiveQuery`, () => { }) }) + it(`pools a reactive params query only when it passes pooledLiveQueries`, async () => { + await TestBed.runInInjectionContext(async () => { + const source = (id: string) => + createCollection( + mockSyncCollectionOptions({ + id, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + const query = + (collection: Collection) => + ({ params, q }: { params: { team: number }; q: any }) => + q + .from({ persons: collection }) + .where(({ persons }: any) => eq(persons.team, `team${params.team}`)) + const pooledSource = source(`reactive-params-pooled-angular`) + const compiledSource = source(`reactive-params-compiled-angular`) + const results = [1, 2].flatMap((team) => [ + injectLiveQuery({ + params: () => ({ team }), + query: query(pooledSource), + pool: pooledLiveQueries, + }), + injectLiveQuery({ + params: () => ({ team }), + query: query(compiledSource), + }), + ]) + + await waitForAngularUpdate() + for (const result of results) result.data() + + // Pooled queries of one shape share the partition's one subscription. + expect(pooledSource.subscriberCount).toBe(1) + expect(compiledSource.subscriberCount).toBe(2) + const ids = (index: number) => + (results[index]!.data() as Array).map((row) => row.id) + expect(ids(0)).toEqual(ids(1)) + }) + }) + it(`accepts reactive params with query function`, async () => { await TestBed.runInInjectionContext(async () => { const collection = createCollection( diff --git a/packages/db/src/live-query-options.ts b/packages/db/src/live-query-options.ts index 6351e8a2a4..1d1e54f5a2 100644 --- a/packages/db/src/live-query-options.ts +++ b/packages/db/src/live-query-options.ts @@ -16,6 +16,7 @@ import type { Context, InitialQueryBuilder, LiveQueryCollectionConfig, + LiveQueryHookConfig, QueryBuilder, } from './query/index.js' @@ -47,7 +48,7 @@ export function resumeDeferredLiveQueryCollections( } type PreparedLiveQueryConfigInput = Omit< - LiveQueryCollectionConfig, + LiveQueryHookConfig, `query` > & { query: @@ -113,6 +114,7 @@ export function prepareLiveQueryValue( query, queryKey: _queryKey, client: _client, + pool, ...config } = value as PreparedLiveQueryConfigInput @@ -128,6 +130,9 @@ export function prepareLiveQueryValue( return { ...config, + // A DbClient query never pools, so its identity must not depend on + // pool: a server preload and a client hook must hash alike. + ...(pool && !dbClient ? { pool } : {}), query: preparedQuery, } } @@ -176,7 +181,10 @@ export function getPreparedLiveQueryIdentity( const config = value as LiveQueryCollectionConfig return [ `config`, - getPreparedLiveQueryIdentity(config.query, config.pool), + getPreparedLiveQueryIdentity( + config.query, + (config as LiveQueryHookConfig).pool, + ), [`getKey`, config.getKey], [`schema`, config.schema], [`singleResult`, config.singleResult === true], @@ -255,8 +263,7 @@ export function resolveLiveQueryValue( ) } if (typeof value === `object`) { - const { pool: configPool, ...given } = - value as LiveQueryCollectionConfig + const { pool: configPool, ...given } = value as LiveQueryHookConfig const pooling = pool === false ? undefined : (configPool ?? pool) // A pooled view needs the built query. Build a query function once and // compile the same builder if the partition cannot serve it. diff --git a/packages/db/src/query/builder/join-operators.ts b/packages/db/src/query/builder/join-operators.ts index e426e3ddf1..908dc73e39 100644 --- a/packages/db/src/query/builder/join-operators.ts +++ b/packages/db/src/query/builder/join-operators.ts @@ -32,7 +32,20 @@ export interface JoinOperator< validate: typeof validateJoinConditions } -function joinOperator(type: TJoinType) { +/** A function that builds one join type's operator. */ +export type JoinOperatorFunction = < + TContext extends Context, + TSource extends Source, +>( + source: TSource, + on: JoinOnCallback< + MergeContextForJoinCallback> + >, +) => JoinOperator + +function joinOperator( + type: TJoinType, +): JoinOperatorFunction { return ( source: TSource, on: JoinOnCallback< @@ -49,10 +62,10 @@ function joinOperator(type: TJoinType) { }) } -export const leftJoin = joinOperator(`left`) -export const rightJoin = joinOperator(`right`) -export const innerJoin = joinOperator(`inner`) -export const fullJoin = joinOperator(`full`) +export const leftJoin: JoinOperatorFunction<`left`> = joinOperator(`left`) +export const rightJoin: JoinOperatorFunction<`right`> = joinOperator(`right`) +export const innerJoin: JoinOperatorFunction<`inner`> = joinOperator(`inner`) +export const fullJoin: JoinOperatorFunction<`full`> = joinOperator(`full`) /** A join whose type is chosen at run time. */ export function joinOf< diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index 3403200014..6b3dde491a 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -112,7 +112,10 @@ export { // One-shot query execution export { queryOnce, type QueryOnceConfig } from './query-once.js' -export { type LiveQueryCollectionConfig } from './live/types.js' +export { + type LiveQueryCollectionConfig, + type LiveQueryHookConfig, +} from './live/types.js' export { type LiveQueryCollectionUtils } from './live/collection-config-builder.js' export { UnhashableQueryIRError, diff --git a/packages/db/src/query/live/types.ts b/packages/db/src/query/live/types.ts index 6e12748736..1e09a30da5 100644 --- a/packages/db/src/query/live/types.ts +++ b/packages/db/src/query/live/types.ts @@ -63,6 +63,19 @@ export type FullSyncState = Required> & * } * ``` */ +/** + * A live query config that a framework hook accepts. Pass `pooledLiveQueries` + * as `pool` to serve the query from a shared equality partition when its + * shape allows. A config with any option other than `query`, `gcTime`, and + * `pool` compiles its own Collection. + */ +export type LiveQueryHookConfig< + TContext extends Context, + TResult extends object = RootQueryResult, +> = LiveQueryCollectionConfig & { + pool?: PooledLiveQueries +} + export interface LiveQueryCollectionConfig< TContext extends Context, TResult extends object = RootQueryResult, @@ -73,13 +86,6 @@ export interface LiveQueryCollectionConfig< */ id?: string - /** - * Framework hooks only: pass `pooledLiveQueries` to serve this query from a - * shared equality partition when its shape allows. A config with any option - * other than `query`, `gcTime`, and `pool` compiles its own Collection. - * `createLiveQueryCollection` does not pool. - */ - pool?: PooledLiveQueries /** * Query builder function that defines the live query diff --git a/packages/db/tests/live-query-options.test.ts b/packages/db/tests/live-query-options.test.ts index d6958b1afd..3482b23759 100644 --- a/packages/db/tests/live-query-options.test.ts +++ b/packages/db/tests/live-query-options.test.ts @@ -473,3 +473,19 @@ describe(`pooling is opt-in`, () => { ).toBeDefined() }) }) + +describe(`pool does not change a DbClient query's identity`, () => { + it(`hashes a preload and a pooling hook config alike`, () => { + const client = new DbClient() + const rows = createCollection<{ id: number; group: number }>({ + id: `pool-identity-${Math.random()}`, + getKey: (row) => row.id, + sync: { sync: ({ markReady }) => markReady() }, + }) + const query = (q: any) => + q.from({ row: rows }).where(({ row }: any) => eq(row.group, 1)) + const hash = (value: unknown) => + getLiveQueryHash(prepareLiveQueryValue(value, client)) + expect(hash({ query, pool: pooledLiveQueries })).toBe(hash({ query })) + }) +}) diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index 745cdba1ac..8f56a96316 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -34,6 +34,7 @@ import type { LiveQueryObserver, LiveQueryPersistedStatus, NonSingleResult, + PooledLiveQueries, QueryBuilder, SingleResult, } from '@tanstack/db' @@ -119,6 +120,11 @@ type UseLiveQueryConfigOptions = Omit< queryKey?: LiveQueryKey /** Override the nearest DbProvider for this query. */ client?: DbClient + /** + * Pass `pooledLiveQueries` to share one partition among equality-filtered + * queries of one shape. Suspense and DbClient queries never pool. + */ + pool?: PooledLiveQueries } type ConfiguredQueryBuilder = Extract< diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 880bd773e1..15e4080a15 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -25,7 +25,7 @@ import type { GetResult, InferResultType, InitialQueryBuilder, - LiveQueryCollectionConfig, + LiveQueryHookConfig, LiveQueryObserver, LiveQueryPersistedStatus, NonSingleResult, @@ -197,7 +197,7 @@ export function useLiveQuery( */ // Overload 2: Accept config object export function useLiveQuery( - config: Accessor>, + config: Accessor>, ): Accessor> & { /** * @deprecated use function result instead diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index 8dc06b201f..6df2cc4a32 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -26,7 +26,7 @@ import type { GetResult, InferResultType, InitialQueryBuilder, - LiveQueryCollectionConfig, + LiveQueryHookConfig, LiveQueryKey, LiveQueryObserver, LiveQueryPersistedStatus, @@ -106,7 +106,7 @@ export interface UseLiveQueryReturnWithCollection< type MaybeGetter = T | (() => T) export type UseLiveQueryConfig = - LiveQueryCollectionConfig & { + LiveQueryHookConfig & { queryKey?: MaybeGetter client?: DbClient } diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 5eed6eecb0..77fffa5d8c 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -24,7 +24,7 @@ import type { GetResult, InferResultType, InitialQueryBuilder, - LiveQueryCollectionConfig, + LiveQueryHookConfig, LiveQueryObserver, LiveQueryPersistedStatus, NonSingleResult, @@ -221,7 +221,7 @@ export function useLiveQuery( */ // Overload 2: Accept config object export function useLiveQuery( - config: LiveQueryCollectionConfig, + config: LiveQueryHookConfig, deps?: Array>, ): UseLiveQueryReturn diff --git a/scripts/bench/incremental-update.ts b/scripts/bench/incremental-update.ts index c9362604c0..5599909503 100644 --- a/scripts/bench/incremental-update.ts +++ b/scripts/bench/incremental-update.ts @@ -11,12 +11,26 @@ import { eq, materialize, } from '../../packages/db/src/index.js' +import * as db from '../../packages/db/src/index.js' import type { ChangeMessageOrDeleteKeyMessage, Collection, SyncConfig, } from '../../packages/db/src/index.js' +// CI runs this script against the base commit too. Before join operators, +// `.join()` took the source and condition directly. +// Before include(), a bare subquery in select was a Collection include. +const includeWith = (builder: any): any => + typeof (db as { include?: unknown }).include === `function` + ? (db as any).include(builder) + : builder + +const joinWith = (builder: any, source: object, on: (refs: any) => any): any => + typeof (db as { leftJoin?: unknown }).leftJoin === `function` + ? builder.join((db as any).leftJoin(source, on)) + : builder.join(source, on) + type Issue = { id: number status: `open` | `closed` @@ -200,15 +214,16 @@ const queryCases: Array = [ scenario: `visible author update`, createQuery: ({ issues, users }) => createLiveQueryCollection((q) => - q - .from({ issue: issues }) - .where(({ issue }) => eq(issue.status, `open`)) - .join({ author: users }, ({ issue, author }) => - eq(issue.authorId, author.id), - ) - .orderBy(({ issue }) => issue.createdAt, `desc`) + joinWith( + q + .from({ issue: issues }) + .where(({ issue }) => eq(issue.status, `open`)), + { author: users }, + ({ issue, author }: any) => eq(issue.authorId, author.id), + ) + .orderBy(({ issue }: any) => issue.createdAt, `desc`) .limit(50) - .select(({ issue, author }) => ({ + .select(({ issue, author }: any) => ({ id: issue.id, title: issue.title, createdAt: issue.createdAt, @@ -254,16 +269,18 @@ const queryCases: Array = [ .select(({ issue }) => ({ id: issue.id, title: issue.title, - recentComments: q - .from({ comment: comments }) - .where(({ comment }) => eq(comment.issueId, issue.id)) - .orderBy(({ comment }) => comment.createdAt, `desc`) - .limit(3) - .select(({ comment }) => ({ - id: comment.id, - body: comment.body, - createdAt: comment.createdAt, - })), + recentComments: includeWith( + q + .from({ comment: comments }) + .where(({ comment }) => eq(comment.issueId, issue.id)) + .orderBy(({ comment }) => comment.createdAt, `desc`) + .limit(3) + .select(({ comment }) => ({ + id: comment.id, + body: comment.body, + createdAt: comment.createdAt, + })), + ), })), ) as unknown as BenchmarkCollection, write: insertVisibleComment, @@ -279,15 +296,17 @@ const queryCases: Array = [ .select(({ issue }) => ({ id: issue.id, title: issue.title, - comments: q - .from({ comment: comments }) - .where(({ comment }) => eq(comment.issueId, issue.id)) - .orderBy(({ comment }) => comment.createdAt, `desc`) - .select(({ comment }) => ({ - id: comment.id, - body: comment.body, - createdAt: comment.createdAt, - })), + comments: includeWith( + q + .from({ comment: comments }) + .where(({ comment }) => eq(comment.issueId, issue.id)) + .orderBy(({ comment }) => comment.createdAt, `desc`) + .select(({ comment }) => ({ + id: comment.id, + body: comment.body, + createdAt: comment.createdAt, + })), + ), })), ) as unknown as BenchmarkCollection, write: insertSelectedIssueComment, diff --git a/scripts/test-minified-db.mjs b/scripts/test-minified-db.mjs index 03cbaad5b8..d725b77a93 100644 --- a/scripts/test-minified-db.mjs +++ b/scripts/test-minified-db.mjs @@ -91,8 +91,8 @@ const errorGroups = { ThrottleCallDroppedError: `ThrottleCallDroppedError`, DebounceCallDroppedError: `DebounceCallDroppedError`, OnMutateMustBeSynchronousError: `OnMutateMustBeSynchronousError`, - QueryBuilderError: `QueryBuilderError OnlyOneSourceAllowedError SubQueryMustHaveFromClauseError InvalidSourceError InvalidSourceTypeError JoinConditionMustBeEqualityError QueryMustHaveFromClauseError InvalidWhereExpressionError`, - QueryCompilationError: `QueryCompilationError DistinctRequiresSelectError FnSelectWithGroupByError UnsupportedFnSelectResultError UnsupportedRootScalarSelectError HavingRequiresGroupByError LimitOffsetRequireOrderByError CollectionInputNotFoundError DuplicateAliasInSubqueryError UnsupportedFromTypeError UnknownExpressionTypeError EmptyReferencePathError UnknownFunctionError JoinCollectionNotFoundError MissingAliasInputsError SetWindowRequiresOrderByError SetWindowFindOneLimitError`, + QueryBuilderError: `QueryBuilderError OnlyOneSourceAllowedError SubQueryMustHaveFromClauseError InvalidSourceError InvalidSourceTypeError JoinConditionMustBeEqualityError QueryMustHaveFromClauseError InvalidWhereExpressionError JoinOperatorRequiredError IncludeOperatorRequiredError`, + QueryCompilationError: `QueryCompilationError DistinctRequiresSelectError FnSelectWithGroupByError UnsupportedFnSelectResultError UnsupportedRootScalarSelectError HavingRequiresGroupByError LimitOffsetRequireOrderByError CollectionInputNotFoundError DuplicateAliasInSubqueryError UnsupportedFromTypeError UnknownExpressionTypeError EmptyReferencePathError UnknownFunctionError JoinCollectionNotFoundError MissingAliasInputsError SetWindowRequiresOrderByError SetWindowFindOneLimitError GroupingOperatorRequiredError`, UnsafeAliasPathError: `UnsafeAliasPathError`, JoinError: `JoinError UnsupportedJoinTypeError InvalidJoinConditionSameSourceError InvalidJoinConditionSourceMismatchError InvalidJoinConditionLeftSourceError InvalidJoinConditionRightSourceError InvalidJoinCondition UnsupportedJoinSourceTypeError`, GroupByError: `GroupByError NonAggregateExpressionNotInGroupByError UnsupportedAggregateFunctionError AggregateFunctionNotInSelectError UnknownHavingExpressionTypeError`, From f0668bffe080b52e1f9b23a1f2735d72e583601c Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 21:30:18 -0600 Subject: [PATCH 11/32] refactor(db): carry one join runtime; join apps skip the include materializer A join clause carries one runtime object (compiler, pushdown, validation, and public-key reduction), and the optimizer copies it by spreading the clause. Public-key reduction moves to its own module, so a joined query with a custom key no longer bundles the include materializer. The bundle test now tracks the materializer and the shared reduction module. Co-authored-by: Isaac --- docs/errors.md | 6 +- packages/db/src/query/builder/index.ts | 9 +- .../db/src/query/builder/join-operators.ts | 16 +- packages/db/src/query/compiler/index.ts | 2 +- .../db/src/query/compiler/join-runtime.ts | 18 ++ packages/db/src/query/ir.ts | 9 +- .../query/live/collection-config-builder.ts | 19 +- .../src/query/live/materialized-pipeline.ts | 147 ++------------- .../db/src/query/live/public-key-reduction.ts | 163 +++++++++++++++++ packages/db/src/query/optimizer-joins.ts | 6 +- packages/db/src/query/optimizer.ts | 2 +- .../tests/fixtures/error-site-messages.json | 6 +- .../db/tests/opt-in-operator-bundle.test.ts | 12 +- .../collection-ref-compatibility.test.ts | 8 +- .../db/tests/query/compiler/basic.test.ts | 7 +- .../tests/query/compiler/lazy-demand.test.ts | 8 +- .../query/compiler/subquery-caching.test.ts | 16 +- .../identity-output-shape-oracle.test.ts | 8 +- .../query/ir-stable-identity-oracle.test.ts | 12 +- .../query/optimizer-semantics-oracle.test.ts | 48 ++--- packages/db/tests/query/optimizer.test.ts | 168 +++++------------- 21 files changed, 301 insertions(+), 389 deletions(-) create mode 100644 packages/db/src/query/compiler/join-runtime.ts create mode 100644 packages/db/src/query/live/public-key-reduction.ts diff --git a/docs/errors.md b/docs/errors.md index ec6494190f..e45169eac3 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -1688,7 +1688,7 @@ Query '${this.id}' has no collection sources. This should not happen; please rep -## Error 154: `query/live/materialized-pipeline.ts` +## Error 154: `query/live/public-key-reduction.ts` Development builds show this message, with `${...}` replaced by values: @@ -1698,7 +1698,7 @@ Canonical query row has negative multiplicity -## Error 155: `query/live/materialized-pipeline.ts` +## Error 155: `query/live/public-key-reduction.ts` Development builds show this message, with `${...}` replaced by values: @@ -1708,7 +1708,7 @@ Canonical query row has no positive contributor -## Error 156: `query/live/materialized-pipeline.ts` +## Error 156: `query/live/public-key-reduction.ts` Development builds show this message, with `${...}` replaced by values: diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index ae89ebab41..c46f5dfafe 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -347,7 +347,8 @@ export class BaseQueryBuilder { if ( typeof value !== `object` || value === null || - typeof (value as { compile?: unknown }).compile !== `function` + typeof (value as { runtime?: { compile?: unknown } }).runtime?.compile !== + `function` ) throw new JoinOperatorRequiredError( value === null ? `null` : typeof value, @@ -370,14 +371,12 @@ export class BaseQueryBuilder { // Get the join condition expression const onExpression = operator.on(refProxy) - operator.validate(onExpression) + operator.runtime.validate(onExpression) const joinClause: JoinClause = { from, type: operator.type, on: onExpression, - compile: operator.compile, - optimize: operator.optimize, - materialize: operator.materialize, + runtime: operator.runtime, } const existingJoins = this.query.join || [] diff --git a/packages/db/src/query/builder/join-operators.ts b/packages/db/src/query/builder/join-operators.ts index 908dc73e39..b61465ea4d 100644 --- a/packages/db/src/query/builder/join-operators.ts +++ b/packages/db/src/query/builder/join-operators.ts @@ -1,7 +1,5 @@ -import { processJoins } from '../compiler/joins.js' -import { validateJoinConditions } from '../join-conditions.js' -import { applyOptimizations } from '../optimizer-joins.js' -import { materializeCompilation } from '../live/materialized-pipeline.js' +import { joinRuntime } from '../compiler/join-runtime.js' +import type { JoinRuntime } from '../compiler/join-runtime.js' import type { Context, JoinOnCallback, @@ -26,10 +24,7 @@ export interface JoinOperator< MergeContextForJoinCallback> > type: TJoinType - compile: typeof processJoins - optimize: typeof applyOptimizations - materialize: typeof materializeCompilation - validate: typeof validateJoinConditions + runtime: JoinRuntime } /** A function that builds one join type's operator. */ @@ -55,10 +50,7 @@ function joinOperator( source, on, type, - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, - validate: validateJoinConditions, + runtime: joinRuntime, }) } diff --git a/packages/db/src/query/compiler/index.ts b/packages/db/src/query/compiler/index.ts index 6281efffb2..53205ae9eb 100644 --- a/packages/db/src/query/compiler/index.ts +++ b/packages/db/src/query/compiler/index.ts @@ -595,7 +595,7 @@ function compileQueryInternal( // Process JOIN clauses if they exist if (query.join && query.join.length > 0) { - pipeline = query.join[0]!.compile( + pipeline = query.join[0]!.runtime.compile( pipeline, query.join, sources, diff --git a/packages/db/src/query/compiler/join-runtime.ts b/packages/db/src/query/compiler/join-runtime.ts new file mode 100644 index 0000000000..7324acb012 --- /dev/null +++ b/packages/db/src/query/compiler/join-runtime.ts @@ -0,0 +1,18 @@ +import { reducePublicKeys } from '../live/public-key-reduction.js' +import { validateJoinConditions } from '../join-conditions.js' +import { applyOptimizations } from '../optimizer-joins.js' +import { processJoins } from './joins.js' + +/** + * The join compiler and its helpers. Join operators carry it onto each IR join + * clause, so apps without joins do not bundle it. + */ +export const joinRuntime = { + compile: processJoins, + optimize: applyOptimizations, + // A joined query with a custom key reduces its public keys. + reducePublicKeys, + validate: validateJoinConditions, +} + +export type JoinRuntime = typeof joinRuntime diff --git a/packages/db/src/query/ir.ts b/packages/db/src/query/ir.ts index 64beb2e42a..6ce00cd175 100644 --- a/packages/db/src/query/ir.ts +++ b/packages/db/src/query/ir.ts @@ -10,11 +10,9 @@ import type { Collection, CollectionImpl } from '../collection/index.js' import type { CollectionOptionsIdentity } from '../collection-options.js' import type { NamespacedRow } from '../types' -import type { processJoins } from './compiler/joins.js' +import type { JoinRuntime } from './compiler/join-runtime.js' import type { Grouping } from './compiler/grouping.js' import type { IncludesRuntime } from './live/includes-runtime.js' -import type { materializeCompilation } from './live/materialized-pipeline.js' -import type { applyOptimizations } from './optimizer-joins.js' export interface QueryIR { from: From @@ -59,10 +57,7 @@ export interface JoinClause { on: BasicExpression // The join operator value that built this clause supplies the compiler, // so apps without joins never import it. - compile: typeof processJoins - optimize: typeof applyOptimizations - // Reduces public keys for a joined query with a custom key. - materialize: typeof materializeCompilation + runtime: JoinRuntime } export type Where = diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index b8d5064ef8..cb92e505b5 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -912,14 +912,17 @@ export class CollectionConfigBuilder< // Includes bring the materializer. A joined query with a custom key // also reduces its public keys, so its join operator brings it too. const includesRuntime = compilation.includes?.[0]?.runtime - const materialize = - includesRuntime?.materializeCompilation ?? - (this.config.getKey - ? this.findJoinClause(this.query)?.materialize - : undefined) - const materialized = materialize - ? materialize(compilation, this.config.getKey, this.hasJoins(this.query)) - : { pipeline: compilation.pipeline, facades: undefined } + const joinClause = this.findJoinClause(this.query) + const getKey = this.config.getKey + const materialized = includesRuntime + ? includesRuntime.materializeCompilation( + compilation, + getKey, + joinClause !== undefined, + ) + : getKey && joinClause + ? joinClause.runtime.reducePublicKeys(compilation, getKey) + : { pipeline: compilation.pipeline, facades: undefined } this.pipelineCache = materialized.pipeline this.sourceWhereClausesCache = compilation.sourceWhereClauses this.bucketFacadesCache = materialized.facades diff --git a/packages/db/src/query/live/materialized-pipeline.ts b/packages/db/src/query/live/materialized-pipeline.ts index 455fa391eb..e515ecffec 100644 --- a/packages/db/src/query/live/materialized-pipeline.ts +++ b/packages/db/src/query/live/materialized-pipeline.ts @@ -7,10 +7,12 @@ import { reduce, serializeValue, } from '@tanstack/db-ivm' -import { deepEquals } from '../../utils.js' -import { getParentContextIdentity } from '../equality-value-identity.js' -import { INCLUDES_ROUTING } from '../compiler/route-metadata.js' import { codedMessage, devBuild } from '../../error-message.js' +import { + canonicalizeByPublicKey, + exposeRouting, + routeKey, +} from './public-key-reduction.js' import type { ValueIdentity } from '../equality-value-identity.js' import type { CompilationResult, @@ -19,28 +21,11 @@ import type { import type { IncludesMaterialization } from '../ir.js' import type { IStreamBuilder } from '@tanstack/db-ivm' import type { ResultStream } from '../../types.js' - -type ResultTuple = [ - value: Record, - order: string | undefined, - correlationKey?: unknown, - parentContext?: Record | null, - routing?: IncludesRouting, - publicKey?: unknown, -] - -type IncludesRouting = Record - -type IncludeRoute = { - active: boolean - correlationKey: unknown - parentContext: Record | null -} - -type CanonicalResult = { - publicKey: unknown - tuple: ResultTuple -} +import type { + IncludeRoute, + RelationScope, + ResultTuple, +} from './public-key-reduction.js' // A bucket row leads with its public key. The D2 index tells apart values // with a different leading string, number or bigint without hashing them, so @@ -83,8 +68,6 @@ export type MaterializedRootCompilation = { facades: Array | undefined } -type RelationScope = `root` | `child` - type BuiltRelations = WeakMap< CompilationResult, Partial> @@ -198,105 +181,6 @@ function dedupeFacades( return [...new Map(facades.map((facade) => [facade.edgeId, facade])).values()] } -function exposeRouting(pipeline: ResultStream): ResultStream { - return pipeline.pipe( - map(([key, rawTuple]) => { - const tuple = rawTuple as ResultTuple - return [ - key, - [ - tuple[0], - tuple[1], - tuple[2], - tuple[3], - tuple[0][INCLUDES_ROUTING], - tuple[4], - ], - ] - }), - ) as unknown as ResultStream -} - -function canonicalizeByPublicKey( - pipeline: ResultStream, - getKey: ((row: any) => unknown) | undefined, - scope: RelationScope, - valueIdentity: ValueIdentity, -): ResultStream { - return pipeline.pipe( - map(([internalKey, rawTuple]) => { - const tuple = rawTuple as ResultTuple - const publicKey = getKey ? getKey(tuple[0]) : (tuple[5] ?? internalKey) - const relationKey = - scope === `root` - ? serializeValue([`root`, publicKey]) - : serializeValue([ - routeKey(tuple[2], tuple[3], valueIdentity), - publicKey, - ]) - return [relationKey, { publicKey, tuple }] as [string, CanonicalResult] - }), - reduce((values: Array<[CanonicalResult, number]>) => { - const totalMultiplicity = values.reduce( - (total, [, multiplicity]) => total + multiplicity, - 0, - ) - if (totalMultiplicity === 0) return [] - if (totalMultiplicity < 0) { - throw new Error( - devBuild() && process.env.NODE_ENV !== `production` - ? `Canonical query row has negative multiplicity` - : codedMessage(154), - ) - } - - const visible = values.find(([, multiplicity]) => multiplicity > 0)?.[0] - if (!visible) { - throw new Error( - devBuild() && process.env.NODE_ENV !== `production` - ? `Canonical query row has no positive contributor` - : codedMessage(155), - ) - } - - for (const [candidate, multiplicity] of values) { - if (multiplicity <= 0) continue - assertCongruentContributors(visible, candidate) - } - - return [[visible, 1]] - }), - map(([relationKey, { publicKey, tuple }]) => [ - scope === `root` ? publicKey : relationKey, - tuple, - ]), - ) as ResultStream -} - -function assertCongruentContributors( - left: CanonicalResult, - right: CanonicalResult, -): void { - const [leftValue, leftOrder, leftCorrelation, leftContext, leftRouting] = - left.tuple - const [rightValue, rightOrder, rightCorrelation, rightContext, rightRouting] = - right.tuple - - if ( - leftOrder !== rightOrder || - !deepEquals(leftValue, rightValue) || - !deepEquals(leftCorrelation, rightCorrelation) || - !deepEquals(leftContext, rightContext) || - !deepEquals(leftRouting, rightRouting) - ) { - throw new Error( - devBuild() && process.env.NODE_ENV !== `production` - ? `Query contributors for public key ${serializeValue(left.publicKey)} are not congruent` - : codedMessage(156, { publicKey: left.publicKey }), - ) - } -} - function attachInlineInclude( parentPipeline: ResultStream, bucketRows: IStreamBuilder<[string, BucketRow]>, @@ -470,17 +354,6 @@ function getIncludeRoute( return tuple[4]?.[fieldName] } -function routeKey( - correlationKey: unknown, - parentContext: Record | null | undefined, - valueIdentity: ValueIdentity, -): string { - return serializeValue([ - valueIdentity.equality(correlationKey ?? null), - getParentContextIdentity(parentContext ?? null), - ]) -} - function compareBucketRows(left: BucketRow, right: BucketRow): number { const [leftPublicKey, , leftOrder] = left const [rightPublicKey, , rightOrder] = right diff --git a/packages/db/src/query/live/public-key-reduction.ts b/packages/db/src/query/live/public-key-reduction.ts new file mode 100644 index 0000000000..90072b9bcd --- /dev/null +++ b/packages/db/src/query/live/public-key-reduction.ts @@ -0,0 +1,163 @@ +import { map, reduce, serializeValue } from '@tanstack/db-ivm' +import { deepEquals } from '../../utils.js' +import { getParentContextIdentity } from '../equality-value-identity.js' +import { INCLUDES_ROUTING } from '../compiler/route-metadata.js' +import { codedMessage, devBuild } from '../../error-message.js' +import type { ValueIdentity } from '../equality-value-identity.js' +import type { CompilationResult } from '../compiler/index.js' +import type { ResultStream } from '../../types.js' + +// Public-key reduction. A joined query with a custom key needs only this +// part of materialization, so join operators carry it without the include +// materializer. + +export type ResultTuple = [ + value: Record, + order: string | undefined, + correlationKey?: unknown, + parentContext?: Record | null, + routing?: IncludesRouting, + publicKey?: unknown, +] + +export type IncludesRouting = Record + +export type IncludeRoute = { + active: boolean + correlationKey: unknown + parentContext: Record | null +} + +export type CanonicalResult = { + publicKey: unknown + tuple: ResultTuple +} + +export type RelationScope = `root` | `child` + +export function exposeRouting(pipeline: ResultStream): ResultStream { + return pipeline.pipe( + map(([key, rawTuple]) => { + const tuple = rawTuple as ResultTuple + return [ + key, + [ + tuple[0], + tuple[1], + tuple[2], + tuple[3], + tuple[0][INCLUDES_ROUTING], + tuple[4], + ], + ] + }), + ) as unknown as ResultStream +} + +export function canonicalizeByPublicKey( + pipeline: ResultStream, + getKey: ((row: any) => unknown) | undefined, + scope: RelationScope, + valueIdentity: ValueIdentity, +): ResultStream { + return pipeline.pipe( + map(([internalKey, rawTuple]) => { + const tuple = rawTuple as ResultTuple + const publicKey = getKey ? getKey(tuple[0]) : (tuple[5] ?? internalKey) + const relationKey = + scope === `root` + ? serializeValue([`root`, publicKey]) + : serializeValue([ + routeKey(tuple[2], tuple[3], valueIdentity), + publicKey, + ]) + return [relationKey, { publicKey, tuple }] as [string, CanonicalResult] + }), + reduce((values: Array<[CanonicalResult, number]>) => { + const totalMultiplicity = values.reduce( + (total, [, multiplicity]) => total + multiplicity, + 0, + ) + if (totalMultiplicity === 0) return [] + if (totalMultiplicity < 0) { + throw new Error( + devBuild() && process.env.NODE_ENV !== `production` + ? `Canonical query row has negative multiplicity` + : codedMessage(154), + ) + } + + const visible = values.find(([, multiplicity]) => multiplicity > 0)?.[0] + if (!visible) { + throw new Error( + devBuild() && process.env.NODE_ENV !== `production` + ? `Canonical query row has no positive contributor` + : codedMessage(155), + ) + } + + for (const [candidate, multiplicity] of values) { + if (multiplicity <= 0) continue + assertCongruentContributors(visible, candidate) + } + + return [[visible, 1]] + }), + map(([relationKey, { publicKey, tuple }]) => [ + scope === `root` ? publicKey : relationKey, + tuple, + ]), + ) as ResultStream +} + +export function assertCongruentContributors( + left: CanonicalResult, + right: CanonicalResult, +): void { + const [leftValue, leftOrder, leftCorrelation, leftContext, leftRouting] = + left.tuple + const [rightValue, rightOrder, rightCorrelation, rightContext, rightRouting] = + right.tuple + + if ( + leftOrder !== rightOrder || + !deepEquals(leftValue, rightValue) || + !deepEquals(leftCorrelation, rightCorrelation) || + !deepEquals(leftContext, rightContext) || + !deepEquals(leftRouting, rightRouting) + ) { + throw new Error( + devBuild() && process.env.NODE_ENV !== `production` + ? `Query contributors for public key ${serializeValue(left.publicKey)} are not congruent` + : codedMessage(156, { publicKey: left.publicKey }), + ) + } +} + +export function routeKey( + correlationKey: unknown, + parentContext: Record | null | undefined, + valueIdentity: ValueIdentity, +): string { + return serializeValue([ + valueIdentity.equality(correlationKey ?? null), + getParentContextIdentity(parentContext ?? null), + ]) +} + + +/** Reduces a query without includes to one canonical row per public key. */ +export function reducePublicKeys( + compilation: CompilationResult, + getKey: (row: any) => unknown, +): { pipeline: ResultStream; facades: undefined } { + return { + pipeline: canonicalizeByPublicKey( + exposeRouting(compilation.pipeline), + getKey, + `root`, + compilation.valueIdentity, + ), + facades: undefined, + } +} diff --git a/packages/db/src/query/optimizer-joins.ts b/packages/db/src/query/optimizer-joins.ts index d3ed86b16c..0a16295fd9 100644 --- a/packages/db/src/query/optimizer-joins.ts +++ b/packages/db/src/query/optimizer-joins.ts @@ -150,11 +150,7 @@ function deepCopyQuery(query: QueryIR): QueryIR { from: deepCopyFrom(query.from), join: query.join ? query.join.map((joinClause) => ({ - type: joinClause.type, - on: joinClause.on, - compile: joinClause.compile, - optimize: joinClause.optimize, - materialize: joinClause.materialize, + ...joinClause, from: deepCopyFrom(joinClause.from) as CollectionSourceRef | QueryRefClass, })) diff --git a/packages/db/src/query/optimizer.ts b/packages/db/src/query/optimizer.ts index 67783a2134..7c78cc6031 100644 --- a/packages/db/src/query/optimizer.ts +++ b/packages/db/src/query/optimizer.ts @@ -440,7 +440,7 @@ function applySingleLevelOptimization(query: QueryIR): QueryIR { // Step 4: The join operator supplies the pushdown that lifts single-source // clauses into subqueries, so apps without joins never import it. - return query.join[0]!.optimize(query, groupedClauses) + return query.join[0]!.runtime.optimize(query, groupedClauses) } /** diff --git a/packages/db/tests/fixtures/error-site-messages.json b/packages/db/tests/fixtures/error-site-messages.json index 4dd1df77c5..841a42a137 100644 --- a/packages/db/tests/fixtures/error-site-messages.json +++ b/packages/db/tests/fixtures/error-site-messages.json @@ -440,17 +440,17 @@ ] }, "154": { - "file": "query/live/materialized-pipeline.ts", + "file": "query/live/public-key-reduction.ts", "template": "Canonical query row has negative multiplicity", "literals": ["Canonical query row has negative multiplicity"] }, "155": { - "file": "query/live/materialized-pipeline.ts", + "file": "query/live/public-key-reduction.ts", "template": "Canonical query row has no positive contributor", "literals": ["Canonical query row has no positive contributor"] }, "156": { - "file": "query/live/materialized-pipeline.ts", + "file": "query/live/public-key-reduction.ts", "template": "Query contributors for public key ${serializeValue(left.publicKey)} are not congruent", "literals": ["Query contributors for public key ", " are not congruent"] }, diff --git a/packages/db/tests/opt-in-operator-bundle.test.ts b/packages/db/tests/opt-in-operator-bundle.test.ts index 2a2b72107f..979e9bc616 100644 --- a/packages/db/tests/opt-in-operator-bundle.test.ts +++ b/packages/db/tests/opt-in-operator-bundle.test.ts @@ -22,15 +22,24 @@ import { describe, expect, it } from 'vitest' const index = fileURLToPath(new URL(`../src/index.ts`, import.meta.url)) const modules = { - joins: [`src/query/compiler/joins.ts`, `src/query/optimizer-joins.ts`], + joins: [ + `src/query/compiler/joins.ts`, + `src/query/compiler/join-runtime.ts`, + `src/query/optimizer-joins.ts`, + ], grouping: [`src/query/compiler/grouping.ts`], includes: [ `src/query/live/bucket-facade-adapter.ts`, `src/query/live/includes-runtime.ts`, + `src/query/live/materialized-pipeline.ts`, ], pooling: [`src/query/pooled-live-query.ts`], } as const +// Joins and includes share public-key reduction; an app with neither keeps it +// out. +const shared = [`src/query/live/public-key-reduction.ts`] + /** Bundles an app that exports `names` and returns its modules' output bytes. */ async function bundledModules(names: Array) { const result = await build({ @@ -72,6 +81,7 @@ describe(`opt-in operator bundles`, () => { for (const files of Object.values(modules)) { for (const file of files) expect(bytes.has(file), file).toBe(false) } + for (const file of shared) expect(bytes.has(file), file).toBe(false) // Aggregate detection stays in core; the grouping compiler does not. expect(bytes.get(`src/query/compiler/group-by.ts`) ?? 0).toBeLessThan(1000) }) diff --git a/packages/db/tests/query/collection-ref-compatibility.test.ts b/packages/db/tests/query/collection-ref-compatibility.test.ts index 9bec013bc5..a28400240e 100644 --- a/packages/db/tests/query/collection-ref-compatibility.test.ts +++ b/packages/db/tests/query/collection-ref-compatibility.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'vitest' +import { joinRuntime } from '../../src/query/compiler/join-runtime.js' import { includesRuntime } from '../../src/query/live/includes-runtime.js' -import { materializeCompilation } from '../../src/query/live/materialized-pipeline.js' -import { applyOptimizations } from '../../src/query/optimizer-joins.js' -import { processJoins } from '../../src/query/compiler/joins.js' import { IR, Query, @@ -109,9 +107,7 @@ describe(`CollectionRef public shape`, () => { from: ref(`root`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: ref(`joined`), type: `inner`, on: new IR.Func(`eq`, [ diff --git a/packages/db/tests/query/compiler/basic.test.ts b/packages/db/tests/query/compiler/basic.test.ts index ddee4bafff..44ecc08969 100644 --- a/packages/db/tests/query/compiler/basic.test.ts +++ b/packages/db/tests/query/compiler/basic.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test } from 'vitest' import { D2, MultiSet, output } from '@tanstack/db-ivm' -import { applyOptimizations } from '../../../src/query/optimizer-joins.js' -import { processJoins } from '../../../src/query/compiler/joins.js' +import { joinRuntime } from '../../../src/query/compiler/join-runtime.js' import { compileQuery } from '../../../src/query/compiler/index.js' import { materializeCompilation } from '../../../src/query/live/materialized-pipeline.js' import { CollectionRef, Func, PropRef, Value } from '../../../src/query/ir.js' @@ -214,9 +213,7 @@ describe(`Query2 Compiler`, () => { from: new CollectionRef(usersCollection, userAlias), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `inner`, from: new CollectionRef(postsCollection, postAlias), on: new Func('eq', [ diff --git a/packages/db/tests/query/compiler/lazy-demand.test.ts b/packages/db/tests/query/compiler/lazy-demand.test.ts index dab66c5473..74469b52f4 100644 --- a/packages/db/tests/query/compiler/lazy-demand.test.ts +++ b/packages/db/tests/query/compiler/lazy-demand.test.ts @@ -1,8 +1,6 @@ import { D2, output } from '@tanstack/db-ivm' import { describe, expect, it } from 'vitest' -import { materializeCompilation } from '../../../src/query/live/materialized-pipeline.js' -import { applyOptimizations } from '../../../src/query/optimizer-joins.js' -import { processJoins } from '../../../src/query/compiler/joins.js' +import { joinRuntime } from '../../../src/query/compiler/join-runtime.js' import { createCollection } from '../../../src/collection/index.js' import { compileQuery } from '../../../src/query/compiler/index.js' import { CollectionRef, Func, PropRef } from '../../../src/query/ir.js' @@ -33,9 +31,7 @@ function createDemandHarness( from: new CollectionRef(left, `left`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: joinType, from: new CollectionRef(right, `right`), on: diff --git a/packages/db/tests/query/compiler/subquery-caching.test.ts b/packages/db/tests/query/compiler/subquery-caching.test.ts index fe1d48c2dd..1e76b898c6 100644 --- a/packages/db/tests/query/compiler/subquery-caching.test.ts +++ b/packages/db/tests/query/compiler/subquery-caching.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'vitest' import { D2 } from '@tanstack/db-ivm' -import { materializeCompilation } from '../../../src/query/live/materialized-pipeline.js' -import { applyOptimizations } from '../../../src/query/optimizer-joins.js' -import { processJoins } from '../../../src/query/compiler/joins.js' +import { joinRuntime } from '../../../src/query/compiler/join-runtime.js' import { compileQuery } from '../../../src/query/compiler/index.js' import { queriesMatchForCaching } from '../../../src/query/compiler/query-equivalence.js' import { @@ -107,9 +105,7 @@ describe(`Subquery Caching`, () => { from: new QueryRef(subquery, `main_users`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `inner`, from: new QueryRef(subquery, `joined_users`), // Same subquery object reference on: new Func('eq', [ @@ -386,9 +382,7 @@ describe(`Subquery Caching`, () => { from: new QueryRef(innerSubquery, `inner1`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `left`, from: new QueryRef(innerSubquery, `inner2`), // Same innerSubquery on: new Func('eq', [ @@ -403,9 +397,7 @@ describe(`Subquery Caching`, () => { from: new QueryRef(middleSubquery, `middle`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `inner`, from: new QueryRef(innerSubquery, `direct`), // innerSubquery again at top level on: new Func('eq', [ diff --git a/packages/db/tests/query/identity-output-shape-oracle.test.ts b/packages/db/tests/query/identity-output-shape-oracle.test.ts index a497d4208f..69a1f916cc 100644 --- a/packages/db/tests/query/identity-output-shape-oracle.test.ts +++ b/packages/db/tests/query/identity-output-shape-oracle.test.ts @@ -3,7 +3,7 @@ import { D2, MultiSet, output, serializeValue } from '@tanstack/db-ivm' import { Temporal } from 'temporal-polyfill' import fc from 'fast-check' import { describe, expect, it } from 'vitest' -import { materializeCompilation } from '../../src/query/live/materialized-pipeline.js' +import { joinRuntime } from '../../src/query/compiler/join-runtime.js' import { Query, and, @@ -11,8 +11,6 @@ import { eq, innerJoin, } from '../../src/query/index.js' -import { processJoins } from '../../src/query/compiler/joins.js' -import { applyOptimizations } from '../../src/query/optimizer-joins.js' import { getQueryIR } from '../../src/query/builder/query-ir.js' import { createCollection } from '../../src/collection/index.js' import { count as countRows } from '../../src/query/builder/functions.js' @@ -159,9 +157,7 @@ async function runShapes( from: user, join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: 'inner', from: post, on: new Func('eq', [ diff --git a/packages/db/tests/query/ir-stable-identity-oracle.test.ts b/packages/db/tests/query/ir-stable-identity-oracle.test.ts index fffa65e141..590b5020d1 100644 --- a/packages/db/tests/query/ir-stable-identity-oracle.test.ts +++ b/packages/db/tests/query/ir-stable-identity-oracle.test.ts @@ -2,11 +2,9 @@ import { describe, expect, it, vi } from 'vitest' import { fc, test as fcTest } from '@fast-check/vitest' import { Temporal } from 'temporal-polyfill' import { innerJoin, leftJoin } from '@tanstack/db' +import { joinRuntime } from '../../src/query/compiler/join-runtime.js' import { include } from '../../src/query/index.js' -import { materializeCompilation } from '../../src/query/live/materialized-pipeline.js' import { includesRuntime } from '../../src/query/live/includes-runtime.js' -import { processJoins } from '../../src/query/compiler/joins.js' -import { applyOptimizations } from '../../src/query/optimizer-joins.js' import { CollectionImpl } from '../../src/collection/index.js' import { Query, getQueryIR } from '../../src/query/builder/index.js' import { @@ -875,9 +873,7 @@ function createAlphaRenamedJoinQuery( ), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `inner`, from: new CollectionRef( postsCollection as unknown as ConstructorParameters< @@ -920,9 +916,7 @@ function createAlphaRenamedImplicitJoinQuery( ), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `inner`, from: new CollectionRef( postsCollection as unknown as ConstructorParameters< diff --git a/packages/db/tests/query/optimizer-semantics-oracle.test.ts b/packages/db/tests/query/optimizer-semantics-oracle.test.ts index 842488417d..e657ef2f47 100644 --- a/packages/db/tests/query/optimizer-semantics-oracle.test.ts +++ b/packages/db/tests/query/optimizer-semantics-oracle.test.ts @@ -1,7 +1,5 @@ import { describe, expect, test } from 'vitest' -import { materializeCompilation } from '../../src/query/live/materialized-pipeline.js' -import { applyOptimizations } from '../../src/query/optimizer-joins.js' -import { processJoins } from '../../src/query/compiler/joins.js' +import { joinRuntime } from '../../src/query/compiler/join-runtime.js' import { createLiveQueryCollection, innerJoin, @@ -511,9 +509,7 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: join.type, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -578,9 +574,7 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -589,9 +583,7 @@ describe(`optimizer residual convergence`, () => { ]), }, { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `inner`, from: new CollectionRef(collection, `tag`), on: new Func('eq', [ @@ -639,9 +631,7 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: joinType, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -674,9 +664,7 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -710,9 +698,7 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -742,9 +728,7 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -783,9 +767,7 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -794,9 +776,7 @@ describe(`optimizer residual convergence`, () => { ]), }, { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: laterJoin, from: new CollectionRef(collection, `tag`), on: new Func('eq', [ @@ -858,9 +838,7 @@ describe(`optimizer residual convergence`, () => { ), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -900,9 +878,7 @@ describe(`optimizer residual convergence`, () => { ), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ diff --git a/packages/db/tests/query/optimizer.test.ts b/packages/db/tests/query/optimizer.test.ts index a7c2cbb4b4..fb89d407f4 100644 --- a/packages/db/tests/query/optimizer.test.ts +++ b/packages/db/tests/query/optimizer.test.ts @@ -1,8 +1,6 @@ import { describe, expect, test } from 'vitest' -import { materializeCompilation } from '../../src/query/live/materialized-pipeline.js' +import { joinRuntime } from '../../src/query/compiler/join-runtime.js' import { grouping } from '../../src/query/compiler/grouping.js' -import { applyOptimizations } from '../../src/query/optimizer-joins.js' -import { processJoins } from '../../src/query/compiler/joins.js' import { optimizeQuery } from '../../src/query/optimizer.js' import { Aggregate, @@ -138,9 +136,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -172,9 +168,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -217,9 +211,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new QueryRef(distinctSource, `p`), type: `inner`, on: createEq(createPropRef(`u`, `id`), createPropRef(`p`, `id`)), @@ -241,9 +233,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -292,9 +282,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -334,9 +322,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -374,9 +360,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -414,9 +398,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -460,9 +442,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -523,9 +503,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -560,9 +538,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -604,9 +580,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -644,9 +618,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(existingSubquery, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -692,9 +664,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(deeplyNestedQuery, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -723,9 +693,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -751,9 +719,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -809,9 +775,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -835,9 +799,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -873,9 +835,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -919,9 +879,7 @@ describe(`Query Optimizer`, () => { ), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -970,9 +928,7 @@ describe(`Query Optimizer`, () => { ), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1029,9 +985,7 @@ describe(`Query Optimizer`, () => { ), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1069,9 +1023,7 @@ describe(`Query Optimizer`, () => { ), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new QueryRef( { from: new CollectionRef(mockCollection, `p`), @@ -1152,9 +1104,7 @@ describe(`Query Optimizer`, () => { ), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1188,9 +1138,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1227,9 +1175,7 @@ describe(`Query Optimizer`, () => { // Add JOIN and WHERE to make it optimizable deepQuery.join = [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq(createPropRef(`u`, `id`), createPropRef(`p`, `user_id`)), @@ -1256,9 +1202,7 @@ describe(`Query Optimizer`, () => { ), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1315,9 +1259,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(sharedSubquery, `main_users`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new QueryRef(sharedSubquery, `other_users`), // Same subquery object! type: `inner`, on: createEq( @@ -1378,9 +1320,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1424,9 +1364,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithLimitedOrder, `top_users`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1464,9 +1402,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithSelect, `filtered_users`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1515,9 +1451,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithHaving, `dept_stats`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1553,9 +1487,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithFnSelect, `computed_users`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1604,9 +1536,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithOrderOnly, `sorted_users`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1659,9 +1589,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(safeSubquery, `users`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new QueryRef(unsafeSubquery, `dept_stats`), type: `inner`, on: createEq( @@ -1709,9 +1637,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1772,9 +1698,7 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1865,9 +1789,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(teamsCollection, `team`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `left`, from: new CollectionRef(teamMembersCollection, `teamMember`), on: createEq( @@ -1913,9 +1835,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(usersCollection, `user`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `right`, from: new CollectionRef(profilesCollection, `profile`), on: createEq( @@ -1966,9 +1886,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(ordersCollection, `order`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `full`, from: new CollectionRef(paymentsCollection, `payment`), on: createEq( @@ -2014,9 +1932,7 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(usersCollection, `user`), join: [ { - compile: processJoins, - optimize: applyOptimizations, - materialize: materializeCompilation, + runtime: joinRuntime, type: `inner`, from: new CollectionRef(departmentsCollection, `dept`), on: createEq( From c796f6ef15179d850a7d16c435626cb927489bcf Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 21:36:03 -0600 Subject: [PATCH 12/32] fix(db): reject bare caseWhen subqueries in types and check include() input caseWhen branches take include() for a subquery; the select-object branch no longer accepts a query builder. include() of a non-builder throws a coded error. The compiler finds aggregates with one walker, and the fn.select error names include(). size:compare skips only a base that lacks a scenario's new export. Co-authored-by: Isaac --- docs/errors.md | 20 +++++++++++++ packages/db/src/query/builder/functions.ts | 5 ++-- packages/db/src/query/builder/index.ts | 14 ++++++++++ packages/db/src/query/compiler/index.ts | 13 +++++---- packages/db/src/query/optimizer-joins.ts | 4 +-- .../tests/fixtures/error-site-messages.json | 10 +++++++ packages/db/tests/query/case-when.test-d.ts | 28 +++++++++++++++++++ .../tests/query/functional-variants.test.ts | 17 +++++++++++ packages/db/tests/query/includes.test.ts | 14 ++++++++++ scripts/size-compare.mjs | 8 ++++-- 10 files changed, 122 insertions(+), 11 deletions(-) diff --git a/docs/errors.md b/docs/errors.md index e45169eac3..150b17db77 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -2499,3 +2499,23 @@ Development builds show this message, with `${...}` replaced by values: ```text A windowed or findOne result row has no include route ``` + + + +## Error 248: `query/builder/index.ts` + +Development builds show this message, with `${...}` replaced by values: + +```text +include() must wrap a subquery builder +``` + + + +## Error 249: `query/builder/index.ts` + +Development builds show this message, with `${...}` replaced by values: + +```text +include() must wrap a subquery builder +``` diff --git a/packages/db/src/query/builder/functions.ts b/packages/db/src/query/builder/functions.ts index 56610d80bd..b2f1e59b69 100644 --- a/packages/db/src/query/builder/functions.ts +++ b/packages/db/src/query/builder/functions.ts @@ -42,10 +42,11 @@ type ExpressionLike = export type CaseWhenValue = | ExpressionLike - | QueryBuilder + | IncludeWrapper | ToArrayWrapper | ConcatToArrayWrapper - | Record + // A select object. A query builder is not one: wrap it in include(). + | (Record & { _bindCollectionSources?: never }) type ExtractCaseWhenValue = T extends CaseWhenWrapper ? TResult : T diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index c46f5dfafe..15c198cc80 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -1032,6 +1032,13 @@ function buildNestedSelect( } if (obj instanceof BaseQueryBuilder) throw new IncludeOperatorRequiredError(fieldName) + if (!(obj.query instanceof BaseQueryBuilder)) { + throw new Error( + devBuild() && process.env.NODE_ENV !== `production` + ? `include() must wrap a subquery builder` + : codedMessage(248), + ) + } return buildIncludesSubquery( obj.query, fieldName, @@ -1103,6 +1110,13 @@ function buildNestedSelect( } if (v instanceof BaseQueryBuilder) throw new IncludeOperatorRequiredError(k) if (v instanceof IncludeWrapper) { + if (!(v.query instanceof BaseQueryBuilder)) { + throw new Error( + devBuild() && process.env.NODE_ENV !== `production` + ? `include() must wrap a subquery builder` + : codedMessage(249), + ) + } out[k] = buildIncludesSubquery( v.query, k, diff --git a/packages/db/src/query/compiler/index.ts b/packages/db/src/query/compiler/index.ts index 53205ae9eb..df3d2f2da5 100644 --- a/packages/db/src/query/compiler/index.ts +++ b/packages/db/src/query/compiler/index.ts @@ -39,6 +39,7 @@ import { isRefProxy } from '../builder/ref-proxy-identity.js' import { CaseWhenWrapper, ConcatToArrayWrapper, + IncludeWrapper, MaterializeWrapper, ToArrayWrapper, } from '../builder/functions.js' @@ -67,7 +68,6 @@ import { toBooleanPredicate, } from './evaluators.js' import { registerLazyDemandPlan } from './lazy-demand.js' -import { containsAggregate } from './group-by.js' import { findAggregate } from './find-aggregate.js' import { getLazyLoadTargets } from './lazy-targets.js' import { processOrderBy } from './order-by.js' @@ -120,6 +120,7 @@ function getUnsupportedFnSelectResultDescription( value: unknown, seen: Set = new Set(), ): string | undefined { + if (value instanceof IncludeWrapper) return `include()` if (value instanceof BaseQueryBuilder) return `a child query builder` if (value instanceof ToArrayWrapper) return `toArray()` if (value instanceof ConcatToArrayWrapper) return `concat(toArray())` @@ -725,8 +726,10 @@ function compileQueryInternal( }) } } - const selectHasAggregates = - query.select !== undefined && containsAggregate(query.select) + // One walker finds aggregates; the aggregate it finds carries the grouping + // compiler. + const selectAggregate = findAggregate(query.select) + const selectHasAggregates = selectAggregate !== undefined // A windowed query's or a findOne's includes compile after its top-K and // route only from the rows it shows (law 12); others compile here. const hasOrderBy = (query.orderBy?.length ?? 0) > 0 @@ -1136,9 +1139,9 @@ function compileQueryInternal( groupByMainSource, sourceCarriesInternalRouteState || includesRoutingFns.length > 0, ) - } else if (selectHasAggregates) { + } else if (selectAggregate) { // SELECT contains aggregates but no GROUP BY: implicit single-group aggregation - pipeline = grouping!.processGroupBy( + pipeline = selectAggregate.grouping.processGroupBy( pipeline, [], // Empty group by means single group valueIdentity, diff --git a/packages/db/src/query/optimizer-joins.ts b/packages/db/src/query/optimizer-joins.ts index 0a16295fd9..39a3764664 100644 --- a/packages/db/src/query/optimizer-joins.ts +++ b/packages/db/src/query/optimizer-joins.ts @@ -9,7 +9,7 @@ import { getWhereExpression, isResidualWhere, } from './ir.js' -import { containsAggregate } from './compiler/group-by.js' +import { findAggregate } from './compiler/find-aggregate.js' import { combineWithAnd, getFirstFromAlias, @@ -313,7 +313,7 @@ function isSafeToPushIntoExistingSubquery( return !( query.distinct || (query.select && - (containsAggregate(query.select) || + (findAggregate(query.select) !== undefined || whereReferencesComputedSelectFields( query.select, whereClause, diff --git a/packages/db/tests/fixtures/error-site-messages.json b/packages/db/tests/fixtures/error-site-messages.json index 841a42a137..66873c887b 100644 --- a/packages/db/tests/fixtures/error-site-messages.json +++ b/packages/db/tests/fixtures/error-site-messages.json @@ -928,5 +928,15 @@ "file": "query/compiler/index.ts", "template": "A windowed or findOne result row has no include route", "literals": ["A windowed or findOne result row has no include route"] + }, + "248": { + "file": "query/builder/index.ts", + "template": "include() must wrap a subquery builder", + "literals": ["include() must wrap a subquery builder"] + }, + "249": { + "file": "query/builder/index.ts", + "template": "include() must wrap a subquery builder", + "literals": ["include() must wrap a subquery builder"] } } diff --git a/packages/db/tests/query/case-when.test-d.ts b/packages/db/tests/query/case-when.test-d.ts index 246e07a164..bfce470be6 100644 --- a/packages/db/tests/query/case-when.test-d.ts +++ b/packages/db/tests/query/case-when.test-d.ts @@ -323,3 +323,31 @@ describe(`caseWhen types`, () => { >() }) }) + +describe(`caseWhen include branches`, () => { + test(`rejects a bare subquery branch and accepts include()`, () => { + const users = createUsers() + const posts = createPosts() + createLiveQueryCollection((q) => + q.from({ user: users }).select(({ user }) => ({ + posts: caseWhen( + gt(user.age, 18), + // @ts-expect-error A subquery branch needs include(). + q.from({ post: posts }).where(({ post }) => eq(post.userId, user.id)), + ), + })), + ) + createLiveQueryCollection((q) => + q.from({ user: users }).select(({ user }) => ({ + posts: caseWhen( + gt(user.age, 18), + include( + q + .from({ post: posts }) + .where(({ post }) => eq(post.userId, user.id)), + ), + ), + })), + ) + }) +}) diff --git a/packages/db/tests/query/functional-variants.test.ts b/packages/db/tests/query/functional-variants.test.ts index 3673ad3951..0b687849f5 100644 --- a/packages/db/tests/query/functional-variants.test.ts +++ b/packages/db/tests/query/functional-variants.test.ts @@ -5,6 +5,7 @@ import { createLiveQueryCollection, eq, gt, + include, leftJoin, materialize, toArray, @@ -307,6 +308,22 @@ describe(`Functional Variants Query`, () => { }), ).toThrow(`fn.select() cannot return a child query builder`) + expect(() => + createLiveQueryCollection({ + startSync: true, + query: (q) => + q.from({ user: usersCollection }).fn.select( + (row) => + ({ + id: row.user.id, + departments: include( + q.from({ department: departmentsCollection }), + ), + }) as any, + ), + }), + ).toThrow(`fn.select() cannot return include()`) + const departments = Symbol(`departments`) expect(() => createLiveQueryCollection({ diff --git a/packages/db/tests/query/includes.test.ts b/packages/db/tests/query/includes.test.ts index 04c62247ff..aadc9dff21 100644 --- a/packages/db/tests/query/includes.test.ts +++ b/packages/db/tests/query/includes.test.ts @@ -8042,3 +8042,17 @@ describe(`a subquery in select needs an include operator`, () => { await live.cleanup() }) }) + +describe(`include() argument`, () => { + it(`throws a coded error when include() does not wrap a subquery builder`, () => { + const projects = createProjectsCollection() + expect(() => + createLiveQueryCollection((q) => + q.from({ p: projects }).select(({ p }) => ({ + id: p.id, + issues: include(createIssuesCollection() as any), + })), + ), + ).toThrow(`include() must wrap a subquery builder`) + }) +}) diff --git a/scripts/size-compare.mjs b/scripts/size-compare.mjs index becc98ca96..e6bfd9f1c2 100644 --- a/scripts/size-compare.mjs +++ b/scripts/size-compare.mjs @@ -387,8 +387,12 @@ if (runScenarios) { ) let appGzip for (const scenario of scenarios) { - // A scenario written for a new API cannot build against an older base. - const base = await measureScenario(baseDir, scenario).catch(() => undefined) + // A scenario written for a new API cannot build against an older base: + // it imports a name the base does not export. Any other failure is real. + const base = await measureScenario(baseDir, scenario).catch((error) => { + if (!String(error?.message).includes(`No matching export`)) throw error + return undefined + }) const head = await measureScenario(root, scenario) const baseGzip = base?.get(`(bundle)`).gzip const headGzip = head.get(`(bundle)`).gzip From 922bd9b8575b833425aa6aed7b4ab048c905270d Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 22 Oct 2026 21:45:44 -0600 Subject: [PATCH 13/32] docs: update skills, JSDoc, and changeset for opt-in operators The shipped agent skills use join operators, include(), groupKeys(), and pooledLiveQueries. join() and groupBy() JSDoc describe the new parameters. The changeset covers Angular's params pool and hand-built IR. Oracle files keep their opening contract prose above their imports. Co-authored-by: Isaac --- .changeset/opt-in-operators.md | 4 +- .../reference/functions/injectLiveQuery.md | 30 ++- .../interfaces/InjectLiveQueryResult.md | 26 +-- .../InjectLiveQueryResultWithCollection.md | 26 +-- ...veQueryResultWithSingleResultCollection.md | 26 +-- .../InjectConditionalLiveQueryResult.md | 2 +- .../react/reference/functions/useLiveQuery.md | 22 +- .../ConditionalUseLiveQueryConfig.md | 2 +- .../reference/type-aliases/LiveQueryKey.md | 2 +- .../type-aliases/UseLiveQueryConfig.md | 2 +- .../type-aliases/UseLiveQueryStatus.md | 2 +- .../solid/reference/functions/useLiveQuery.md | 2 +- .../type-aliases/UseLiveQueryConfig.md | 2 +- .../vue/reference/functions/useLiveQuery.md | 2 +- docs/overview.md | 2 +- .../namespaces/IR/classes/Aggregate.md | 14 +- .../namespaces/IR/classes/CollectionRef.md | 16 +- .../IR/classes/ConditionalSelect.md | 12 +- .../namespaces/IR/classes/DescriptorRef.md | 16 +- .../@tanstack/namespaces/IR/classes/Func.md | 12 +- .../namespaces/IR/classes/IncludesSubquery.md | 26 +-- .../namespaces/IR/classes/PropRef.md | 14 +- .../namespaces/IR/classes/QueryRef.md | 14 +- .../namespaces/IR/classes/UnionAll.md | 12 +- .../namespaces/IR/classes/UnionFrom.md | 12 +- .../@tanstack/namespaces/IR/classes/Value.md | 10 +- .../IR/functions/collectCollectionSources.md | 2 +- .../IR/functions/collectPropRefs.md | 2 +- .../IR/functions/collectSourceRefs.md | 2 +- .../IR/functions/createResidualWhere.md | 2 +- .../namespaces/IR/functions/followRef.md | 2 +- .../namespaces/IR/functions/getFromSources.md | 2 +- .../IR/functions/getHavingExpression.md | 2 +- .../IR/functions/getPropRefPropertyPath.md | 2 +- .../IR/functions/getPropRefSourceAlias.md | 2 +- .../IR/functions/getWhereExpression.md | 2 +- .../functions/isBasicOrAggregateExpression.md | 2 +- .../IR/functions/isExpressionLike.md | 2 +- .../IR/functions/isResidualWhere.md | 2 +- .../IR/functions/requireCollectionSource.md | 4 +- .../namespaces/IR/interfaces/JoinClause.md | 186 +++++++++-------- .../namespaces/IR/interfaces/QueryIR.md | 32 +-- .../IR/type-aliases/BasicExpression.md | 2 +- .../IR/type-aliases/CollectionSourceRef.md | 2 +- .../type-aliases/ConditionalSelectBranch.md | 6 +- .../namespaces/IR/type-aliases/From.md | 2 +- .../namespaces/IR/type-aliases/GroupBy.md | 2 +- .../namespaces/IR/type-aliases/Having.md | 2 +- .../type-aliases/IncludesMaterialization.md | 2 +- .../namespaces/IR/type-aliases/Join.md | 2 +- .../namespaces/IR/type-aliases/Limit.md | 2 +- .../namespaces/IR/type-aliases/Offset.md | 2 +- .../namespaces/IR/type-aliases/OrderBy.md | 2 +- .../IR/type-aliases/OrderByClause.md | 6 +- .../IR/type-aliases/OrderByDirection.md | 2 +- .../namespaces/IR/type-aliases/Select.md | 2 +- .../IR/type-aliases/SelectValueExpression.md | 2 +- .../namespaces/IR/type-aliases/Where.md | 2 +- .../IR/variables/INCLUDES_SCALAR_FIELD.md | 2 +- docs/reference/classes/BaseQueryBuilder.md | 44 ++-- docs/reference/functions/add.md | 2 +- docs/reference/functions/and.md | 4 +- docs/reference/functions/avg.md | 6 +- docs/reference/functions/caseWhen.md | 22 +- docs/reference/functions/coalesce.md | 2 +- docs/reference/functions/compileExpression.md | 4 +- docs/reference/functions/compileQuery.md | 2 +- docs/reference/functions/concat.md | 4 +- docs/reference/functions/count.md | 2 +- docs/reference/functions/divide.md | 2 +- docs/reference/functions/eq.md | 6 +- docs/reference/functions/getLiveQueryHash.md | 2 +- .../functions/getPreparedLiveQueryIdentity.md | 2 +- .../functions/getPreparedLiveQuerySources.md | 2 +- docs/reference/functions/gt.md | 6 +- docs/reference/functions/gte.md | 6 +- docs/reference/functions/ilike.md | 2 +- docs/reference/functions/inArray.md | 2 +- docs/reference/functions/isNull.md | 2 +- docs/reference/functions/isUndefined.md | 2 +- docs/reference/functions/joinOf.md | 2 +- docs/reference/functions/length.md | 2 +- docs/reference/functions/like.md | 2 +- docs/reference/functions/lower.md | 2 +- docs/reference/functions/lt.md | 6 +- docs/reference/functions/lte.md | 6 +- docs/reference/functions/max.md | 6 +- docs/reference/functions/min.md | 6 +- docs/reference/functions/multiply.md | 2 +- docs/reference/functions/not.md | 2 +- docs/reference/functions/or.md | 4 +- .../functions/prepareLiveQueryValue.md | 2 +- .../functions/resolveLiveQueryValue.md | 2 +- .../resumeDeferredLiveQueryCollections.md | 2 +- docs/reference/functions/subtract.md | 2 +- docs/reference/functions/sum.md | 6 +- docs/reference/functions/upper.md | 2 +- docs/reference/index.md | 1 + docs/reference/interfaces/JoinOperator.md | 188 +++++++++--------- .../interfaces/LiveQueryCollectionConfig.md | 111 ++--------- .../DeferredLiveQueryCollections.md | 2 +- docs/reference/type-aliases/ExtractContext.md | 2 +- .../type-aliases/InitialQueryBuilder.md | 2 +- docs/reference/type-aliases/JoinType.md | 2 +- .../type-aliases/LiveQueryHookConfig.md | 33 +++ docs/reference/type-aliases/LiveQueryKey.md | 2 +- .../type-aliases/LiveQueryOptions.md | 2 +- docs/reference/type-aliases/OperatorName.md | 2 +- .../type-aliases/PooledLiveQueries.md | 2 +- docs/reference/type-aliases/QueryBuilder.md | 2 +- docs/reference/type-aliases/QueryResult.md | 2 +- docs/reference/variables/Query.md | 2 +- docs/reference/variables/fullJoin.md | 28 +-- docs/reference/variables/innerJoin.md | 28 +-- docs/reference/variables/leftJoin.md | 28 +-- docs/reference/variables/operators.md | 2 +- docs/reference/variables/rightJoin.md | 28 +-- .../angular-db/skills/angular-db/SKILL.md | 6 +- .../src/suites/joins-oracle.suite.ts | 10 +- .../db/skills/db-core/live-queries/SKILL.md | 51 ++--- packages/db/src/query/builder/index.ts | 18 +- packages/db/tests/conformance/suite.ts | 2 +- .../db-client-preload-identity-oracle.test.ts | 4 +- .../tests/initial-readiness-exposure.test.ts | 4 +- ...-query-deferred-acquisition-oracle.test.ts | 2 +- .../query/optimizer-semantics-oracle.test.ts | 16 +- .../query/query-api-type-algebra.test-d.ts | 2 +- .../query/virtual-row-fields-oracle.test-d.ts | 16 +- .../query/virtual-row-fields-oracle.test.ts | 16 +- packages/react-db/skills/react-db/SKILL.md | 8 +- .../descriptor-query-binding-oracle.test.tsx | 28 +-- .../tests/source-id-reuse-oracle.test.tsx | 16 +- packages/solid-db/skills/solid-db/SKILL.md | 6 +- packages/svelte-db/skills/svelte-db/SKILL.md | 2 +- packages/vue-db/skills/vue-db/SKILL.md | 2 +- 135 files changed, 684 insertions(+), 776 deletions(-) create mode 100644 docs/reference/type-aliases/LiveQueryHookConfig.md diff --git a/.changeset/opt-in-operators.md b/.changeset/opt-in-operators.md index 0d35e667be..7ab3b2ccd4 100644 --- a/.changeset/opt-in-operators.md +++ b/.changeset/opt-in-operators.md @@ -4,6 +4,7 @@ '@tanstack/svelte-db': patch '@tanstack/vue-db': patch '@tanstack/solid-db': patch +'@tanstack/angular-db': patch --- Breaking: joins, grouping, includes, and pooling are now opt-in. An app bundles an operator's code only when it imports the value for that operator. A typical app is about 10 kB smaller after gzip. This app has a query collection, a filtered live query, and React `useLiveQuery`. @@ -11,4 +12,5 @@ Breaking: joins, grouping, includes, and pooling are now opt-in. An app bundles - Joins take a join operator. Change `.join(source, on)` to `.join(leftJoin(source, on))`, and `.innerJoin(source, on)` to `.join(innerJoin(source, on))`. `rightJoin`, `fullJoin`, and `joinOf(type, source, on)` work the same way. The old call shape throws `JoinOperatorRequiredError`. - An include needs `include()`, `toArray()`, or `materialize()`. Change `issues: q.from(...)` in `select()` to `issues: include(q.from(...))`. A subquery in `select()` without one of these throws `IncludeOperatorRequiredError`. - Aggregates such as `count()` need no change. A `groupBy` with no aggregate takes `groupKeys()`: `.groupBy(groupKeys(({ todo }) => todo.status))`. Without it, the query throws `GroupingOperatorRequiredError`. -- Hooks do not pool live queries by default. To share one partition among many equality-filtered queries, pass `pool: pooledLiveQueries` in the hook's query config: `useLiveQuery({ query, pool: pooledLiveQueries })`. Results do not change. +- Hooks do not pool live queries by default. To share one partition among many equality-filtered queries, pass `pool: pooledLiveQueries` in the hook's query config: `useLiveQuery({ query, pool: pooledLiveQueries })`. Results do not change. Angular's `{ params, query }` form takes `pool` the same way. +- Code that builds query IR by hand must supply the operator code. `IR.Aggregate` takes the grouping compiler as its third argument. `IR.IncludesSubquery` takes the include runtime as its fifth argument. `IR.JoinClause` has a required `runtime` field. Queries that the query builder builds need no change. diff --git a/docs/framework/angular/reference/functions/injectLiveQuery.md b/docs/framework/angular/reference/functions/injectLiveQuery.md index 0bb18bb03e..f39ecc7dea 100644 --- a/docs/framework/angular/reference/functions/injectLiveQuery.md +++ b/docs/framework/angular/reference/functions/injectLiveQuery.md @@ -9,7 +9,7 @@ title: injectLiveQuery function injectLiveQuery(options): InjectLiveQueryResult; ``` -Defined in: [index.ts:119](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L119) +Defined in: [angular-db/src/index.ts:120](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L120) ### Type Parameters @@ -29,6 +29,13 @@ Defined in: [index.ts:119](https://github.com/TanStack/db/blob/main/packages/ang () => `TParams` +##### pool? + +\{ +\} + +Pass `pooledLiveQueries` to share one partition among equal-shape queries. + ##### query (`args`) => `QueryBuilder`\<`TContext`\> @@ -43,7 +50,7 @@ Defined in: [index.ts:119](https://github.com/TanStack/db/blob/main/packages/ang function injectLiveQuery(options): InjectConditionalLiveQueryResult; ``` -Defined in: [index.ts:129](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L129) +Defined in: [angular-db/src/index.ts:132](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L132) ### Type Parameters @@ -63,6 +70,13 @@ Defined in: [index.ts:129](https://github.com/TanStack/db/blob/main/packages/ang () => `TParams` +##### pool? + +\{ +\} + +Pass `pooledLiveQueries` to share one partition among equal-shape queries. + ##### query (`args`) => `QueryBuilder`\<`TContext`\> \| `null` \| `undefined` @@ -77,7 +91,7 @@ Defined in: [index.ts:129](https://github.com/TanStack/db/blob/main/packages/ang function injectLiveQuery(queryFn): InjectLiveQueryResult; ``` -Defined in: [index.ts:139](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L139) +Defined in: [angular-db/src/index.ts:144](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L144) ### Type Parameters @@ -101,7 +115,7 @@ Defined in: [index.ts:139](https://github.com/TanStack/db/blob/main/packages/ang function injectLiveQuery(queryFn): InjectConditionalLiveQueryResult; ``` -Defined in: [index.ts:142](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L142) +Defined in: [angular-db/src/index.ts:147](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L147) ### Type Parameters @@ -125,7 +139,7 @@ Defined in: [index.ts:142](https://github.com/TanStack/db/blob/main/packages/ang function injectLiveQuery(config): InjectLiveQueryResult; ``` -Defined in: [index.ts:147](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L147) +Defined in: [angular-db/src/index.ts:152](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L152) ### Type Parameters @@ -137,7 +151,7 @@ Defined in: [index.ts:147](https://github.com/TanStack/db/blob/main/packages/ang #### config -`LiveQueryCollectionConfig`\<`TContext`\> +`LiveQueryHookConfig`\<`TContext`\> ### Returns @@ -149,7 +163,7 @@ Defined in: [index.ts:147](https://github.com/TanStack/db/blob/main/packages/ang function injectLiveQuery(liveQueryCollection): InjectLiveQueryResultWithCollection; ``` -Defined in: [index.ts:151](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L151) +Defined in: [angular-db/src/index.ts:156](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L156) ### Type Parameters @@ -181,7 +195,7 @@ Defined in: [index.ts:151](https://github.com/TanStack/db/blob/main/packages/ang function injectLiveQuery(liveQueryCollection): InjectLiveQueryResultWithSingleResultCollection; ``` -Defined in: [index.ts:159](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L159) +Defined in: [angular-db/src/index.ts:164](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L164) ### Type Parameters diff --git a/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md b/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md index a9ffbe32b2..1d9f40e20c 100644 --- a/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md +++ b/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md @@ -3,7 +3,7 @@ id: InjectLiveQueryResult title: InjectLiveQueryResult --- -Defined in: [index.ts:41](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L41) +Defined in: [angular-db/src/index.ts:42](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L42) The result of calling `injectLiveQuery`. Contains reactive signals for the query state and data. @@ -25,7 +25,7 @@ collection: Signal< | null>; ``` -Defined in: [index.ts:47](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L47) +Defined in: [angular-db/src/index.ts:48](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L48) A signal containing the underlying collection instance (null for disabled queries) @@ -37,7 +37,7 @@ A signal containing the underlying collection instance (null for disabled querie data: Signal>; ``` -Defined in: [index.ts:45](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L45) +Defined in: [angular-db/src/index.ts:46](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L46) A signal containing the results as an array, or single result for findOne queries @@ -49,7 +49,7 @@ A signal containing the results as an array, or single result for findOne querie isCleanedUp: Signal; ``` -Defined in: [index.ts:66](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L66) +Defined in: [angular-db/src/index.ts:67](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L67) A signal indicating whether the collection has been cleaned up @@ -61,7 +61,7 @@ A signal indicating whether the collection has been cleaned up isError: Signal; ``` -Defined in: [index.ts:64](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L64) +Defined in: [angular-db/src/index.ts:65](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L65) A signal indicating whether the collection has an error @@ -73,7 +73,7 @@ A signal indicating whether the collection has an error isIdle: Signal; ``` -Defined in: [index.ts:62](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L62) +Defined in: [angular-db/src/index.ts:63](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L63) A signal indicating whether the collection is idle @@ -85,7 +85,7 @@ A signal indicating whether the collection is idle isLoading: Signal; ``` -Defined in: [index.ts:55](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L55) +Defined in: [angular-db/src/index.ts:56](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L56) A signal indicating whether the collection is currently loading @@ -97,7 +97,7 @@ A signal indicating whether the collection is currently loading isPersistedReady: Signal; ``` -Defined in: [index.ts:59](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L59) +Defined in: [angular-db/src/index.ts:60](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L60) *** @@ -107,7 +107,7 @@ Defined in: [index.ts:59](https://github.com/TanStack/db/blob/main/packages/angu isReady: Signal; ``` -Defined in: [index.ts:57](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L57) +Defined in: [angular-db/src/index.ts:58](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L58) A signal indicating whether the collection is ready @@ -119,7 +119,7 @@ A signal indicating whether the collection is ready persistedError: Signal; ``` -Defined in: [index.ts:60](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L60) +Defined in: [angular-db/src/index.ts:61](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L61) *** @@ -129,7 +129,7 @@ Defined in: [index.ts:60](https://github.com/TanStack/db/blob/main/packages/angu persistedStatus: Signal; ``` -Defined in: [index.ts:58](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L58) +Defined in: [angular-db/src/index.ts:59](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L59) *** @@ -139,7 +139,7 @@ Defined in: [index.ts:58](https://github.com/TanStack/db/blob/main/packages/angu state: Signal[K] }>>; ``` -Defined in: [index.ts:43](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L43) +Defined in: [angular-db/src/index.ts:44](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L44) A signal containing the complete state map of results keyed by their ID @@ -151,6 +151,6 @@ A signal containing the complete state map of results keyed by their ID status: Signal; ``` -Defined in: [index.ts:53](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L53) +Defined in: [angular-db/src/index.ts:54](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L54) A signal containing the current status of the collection diff --git a/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithCollection.md b/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithCollection.md index 5892128e83..7e27e27110 100644 --- a/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithCollection.md +++ b/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithCollection.md @@ -3,7 +3,7 @@ id: InjectLiveQueryResultWithCollection title: InjectLiveQueryResultWithCollection --- -Defined in: [index.ts:81](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L81) +Defined in: [angular-db/src/index.ts:82](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L82) ## Type Parameters @@ -30,7 +30,7 @@ collection: Signal< | null>; ``` -Defined in: [index.ts:88](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L88) +Defined in: [angular-db/src/index.ts:89](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L89) *** @@ -40,7 +40,7 @@ Defined in: [index.ts:88](https://github.com/TanStack/db/blob/main/packages/angu data: Signal; ``` -Defined in: [index.ts:87](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L87) +Defined in: [angular-db/src/index.ts:88](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L88) *** @@ -50,7 +50,7 @@ Defined in: [index.ts:87](https://github.com/TanStack/db/blob/main/packages/angu isCleanedUp: Signal; ``` -Defined in: [index.ts:97](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L97) +Defined in: [angular-db/src/index.ts:98](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L98) *** @@ -60,7 +60,7 @@ Defined in: [index.ts:97](https://github.com/TanStack/db/blob/main/packages/angu isError: Signal; ``` -Defined in: [index.ts:96](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L96) +Defined in: [angular-db/src/index.ts:97](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L97) *** @@ -70,7 +70,7 @@ Defined in: [index.ts:96](https://github.com/TanStack/db/blob/main/packages/angu isIdle: Signal; ``` -Defined in: [index.ts:95](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L95) +Defined in: [angular-db/src/index.ts:96](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L96) *** @@ -80,7 +80,7 @@ Defined in: [index.ts:95](https://github.com/TanStack/db/blob/main/packages/angu isLoading: Signal; ``` -Defined in: [index.ts:90](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L90) +Defined in: [angular-db/src/index.ts:91](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L91) *** @@ -90,7 +90,7 @@ Defined in: [index.ts:90](https://github.com/TanStack/db/blob/main/packages/angu isPersistedReady: Signal; ``` -Defined in: [index.ts:93](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L93) +Defined in: [angular-db/src/index.ts:94](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L94) *** @@ -100,7 +100,7 @@ Defined in: [index.ts:93](https://github.com/TanStack/db/blob/main/packages/angu isReady: Signal; ``` -Defined in: [index.ts:91](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L91) +Defined in: [angular-db/src/index.ts:92](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L92) *** @@ -110,7 +110,7 @@ Defined in: [index.ts:91](https://github.com/TanStack/db/blob/main/packages/angu persistedError: Signal; ``` -Defined in: [index.ts:94](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L94) +Defined in: [angular-db/src/index.ts:95](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L95) *** @@ -120,7 +120,7 @@ Defined in: [index.ts:94](https://github.com/TanStack/db/blob/main/packages/angu persistedStatus: Signal; ``` -Defined in: [index.ts:92](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L92) +Defined in: [angular-db/src/index.ts:93](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L93) *** @@ -130,7 +130,7 @@ Defined in: [index.ts:92](https://github.com/TanStack/db/blob/main/packages/angu state: Signal>; ``` -Defined in: [index.ts:86](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L86) +Defined in: [angular-db/src/index.ts:87](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L87) *** @@ -140,4 +140,4 @@ Defined in: [index.ts:86](https://github.com/TanStack/db/blob/main/packages/angu status: Signal; ``` -Defined in: [index.ts:89](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L89) +Defined in: [angular-db/src/index.ts:90](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L90) diff --git a/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithSingleResultCollection.md b/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithSingleResultCollection.md index e456bd589b..e29993c148 100644 --- a/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithSingleResultCollection.md +++ b/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithSingleResultCollection.md @@ -3,7 +3,7 @@ id: InjectLiveQueryResultWithSingleResultCollection title: InjectLiveQueryResultWithSingleResultCollection --- -Defined in: [index.ts:100](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L100) +Defined in: [angular-db/src/index.ts:101](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L101) ## Type Parameters @@ -30,7 +30,7 @@ collection: Signal< | null>; ``` -Defined in: [index.ts:107](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L107) +Defined in: [angular-db/src/index.ts:108](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L108) *** @@ -40,7 +40,7 @@ Defined in: [index.ts:107](https://github.com/TanStack/db/blob/main/packages/ang data: Signal; ``` -Defined in: [index.ts:106](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L106) +Defined in: [angular-db/src/index.ts:107](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L107) *** @@ -50,7 +50,7 @@ Defined in: [index.ts:106](https://github.com/TanStack/db/blob/main/packages/ang isCleanedUp: Signal; ``` -Defined in: [index.ts:116](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L116) +Defined in: [angular-db/src/index.ts:117](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L117) *** @@ -60,7 +60,7 @@ Defined in: [index.ts:116](https://github.com/TanStack/db/blob/main/packages/ang isError: Signal; ``` -Defined in: [index.ts:115](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L115) +Defined in: [angular-db/src/index.ts:116](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L116) *** @@ -70,7 +70,7 @@ Defined in: [index.ts:115](https://github.com/TanStack/db/blob/main/packages/ang isIdle: Signal; ``` -Defined in: [index.ts:114](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L114) +Defined in: [angular-db/src/index.ts:115](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L115) *** @@ -80,7 +80,7 @@ Defined in: [index.ts:114](https://github.com/TanStack/db/blob/main/packages/ang isLoading: Signal; ``` -Defined in: [index.ts:109](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L109) +Defined in: [angular-db/src/index.ts:110](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L110) *** @@ -90,7 +90,7 @@ Defined in: [index.ts:109](https://github.com/TanStack/db/blob/main/packages/ang isPersistedReady: Signal; ``` -Defined in: [index.ts:112](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L112) +Defined in: [angular-db/src/index.ts:113](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L113) *** @@ -100,7 +100,7 @@ Defined in: [index.ts:112](https://github.com/TanStack/db/blob/main/packages/ang isReady: Signal; ``` -Defined in: [index.ts:110](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L110) +Defined in: [angular-db/src/index.ts:111](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L111) *** @@ -110,7 +110,7 @@ Defined in: [index.ts:110](https://github.com/TanStack/db/blob/main/packages/ang persistedError: Signal; ``` -Defined in: [index.ts:113](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L113) +Defined in: [angular-db/src/index.ts:114](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L114) *** @@ -120,7 +120,7 @@ Defined in: [index.ts:113](https://github.com/TanStack/db/blob/main/packages/ang persistedStatus: Signal; ``` -Defined in: [index.ts:111](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L111) +Defined in: [angular-db/src/index.ts:112](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L112) *** @@ -130,7 +130,7 @@ Defined in: [index.ts:111](https://github.com/TanStack/db/blob/main/packages/ang state: Signal>; ``` -Defined in: [index.ts:105](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L105) +Defined in: [angular-db/src/index.ts:106](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L106) *** @@ -140,4 +140,4 @@ Defined in: [index.ts:105](https://github.com/TanStack/db/blob/main/packages/ang status: Signal; ``` -Defined in: [index.ts:108](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L108) +Defined in: [angular-db/src/index.ts:109](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L109) diff --git a/docs/framework/angular/reference/type-aliases/InjectConditionalLiveQueryResult.md b/docs/framework/angular/reference/type-aliases/InjectConditionalLiveQueryResult.md index 9f231ce34a..85ee782c6c 100644 --- a/docs/framework/angular/reference/type-aliases/InjectConditionalLiveQueryResult.md +++ b/docs/framework/angular/reference/type-aliases/InjectConditionalLiveQueryResult.md @@ -7,7 +7,7 @@ title: InjectConditionalLiveQueryResult type InjectConditionalLiveQueryResult = Omit, "data"> & object; ``` -Defined in: [index.ts:74](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L74) +Defined in: [angular-db/src/index.ts:75](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L75) ## Type Declaration diff --git a/docs/framework/react/reference/functions/useLiveQuery.md b/docs/framework/react/reference/functions/useLiveQuery.md index 9d3e8fbe5c..f4558cc4cf 100644 --- a/docs/framework/react/reference/functions/useLiveQuery.md +++ b/docs/framework/react/reference/functions/useLiveQuery.md @@ -9,7 +9,7 @@ title: useLiveQuery function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:396](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L396) +Defined in: [useLiveQuery.ts:402](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L402) Create a live query using a query function. @@ -196,7 +196,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:416](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L416) +Defined in: [useLiveQuery.ts:422](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L422) Create a live query using a query function. @@ -387,7 +387,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:438](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L438) +Defined in: [useLiveQuery.ts:444](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L444) Create a live query using a query function. @@ -581,7 +581,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:460](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L460) +Defined in: [useLiveQuery.ts:466](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L466) Create a live query using a query function. @@ -780,7 +780,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:486](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L486) +Defined in: [useLiveQuery.ts:492](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L492) Create a live query using a query function. @@ -990,7 +990,7 @@ return ( function useLiveQuery(config): object; ``` -Defined in: [useLiveQuery.ts:557](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L557) +Defined in: [useLiveQuery.ts:563](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L563) Create a live query using configuration object @@ -1134,7 +1134,7 @@ return
{data.length} items loaded
function useLiveQuery(config): object; ``` -Defined in: [useLiveQuery.ts:576](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L576) +Defined in: [useLiveQuery.ts:582](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L582) Create a live query using a query function. @@ -1317,7 +1317,7 @@ return ( function useLiveQuery(config, deps?): object; ``` -Defined in: [useLiveQuery.ts:595](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L595) +Defined in: [useLiveQuery.ts:601](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L601) Create a live query using a query function. @@ -1502,7 +1502,7 @@ return ( function useLiveQuery(config, deps): object; ``` -Defined in: [useLiveQuery.ts:615](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L615) +Defined in: [useLiveQuery.ts:621](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L621) Create a live query using a query function. @@ -1691,7 +1691,7 @@ return ( function useLiveQuery(liveQueryCollection): object; ``` -Defined in: [useLiveQuery.ts:664](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L664) +Defined in: [useLiveQuery.ts:670](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L670) Subscribe to an existing live query collection @@ -1837,7 +1837,7 @@ return
{data.map(item => )}
function useLiveQuery(liveQueryCollection): object; ``` -Defined in: [useLiveQuery.ts:687](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L687) +Defined in: [useLiveQuery.ts:693](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L693) Create a live query using a query function. diff --git a/docs/framework/react/reference/type-aliases/ConditionalUseLiveQueryConfig.md b/docs/framework/react/reference/type-aliases/ConditionalUseLiveQueryConfig.md index 1926ba2bb6..690d43248b 100644 --- a/docs/framework/react/reference/type-aliases/ConditionalUseLiveQueryConfig.md +++ b/docs/framework/react/reference/type-aliases/ConditionalUseLiveQueryConfig.md @@ -7,7 +7,7 @@ title: ConditionalUseLiveQueryConfig type ConditionalUseLiveQueryConfig = UseLiveQueryConfigOptions & object; ``` -Defined in: [useLiveQuery.ts:133](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L133) +Defined in: [useLiveQuery.ts:139](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L139) ## Type Declaration diff --git a/docs/framework/react/reference/type-aliases/LiveQueryKey.md b/docs/framework/react/reference/type-aliases/LiveQueryKey.md index bfce3663b8..6eb9eced44 100644 --- a/docs/framework/react/reference/type-aliases/LiveQueryKey.md +++ b/docs/framework/react/reference/type-aliases/LiveQueryKey.md @@ -7,4 +7,4 @@ title: LiveQueryKey type LiveQueryKey = ReadonlyArray; ``` -Defined in: [useLiveQuery.ts:109](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L109) +Defined in: [useLiveQuery.ts:110](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L110) diff --git a/docs/framework/react/reference/type-aliases/UseLiveQueryConfig.md b/docs/framework/react/reference/type-aliases/UseLiveQueryConfig.md index f42f993e61..5d08e0cd28 100644 --- a/docs/framework/react/reference/type-aliases/UseLiveQueryConfig.md +++ b/docs/framework/react/reference/type-aliases/UseLiveQueryConfig.md @@ -7,7 +7,7 @@ title: UseLiveQueryConfig type UseLiveQueryConfig = UseLiveQueryConfigOptions & Pick, "query">; ``` -Defined in: [useLiveQuery.ts:129](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L129) +Defined in: [useLiveQuery.ts:135](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L135) ## Type Parameters diff --git a/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md b/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md index 4219dcacaa..2ac1cf3448 100644 --- a/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md +++ b/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md @@ -7,4 +7,4 @@ title: UseLiveQueryStatus type UseLiveQueryStatus = CollectionStatus | "disabled"; ``` -Defined in: [useLiveQuery.ts:108](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L108) +Defined in: [useLiveQuery.ts:109](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L109) diff --git a/docs/framework/solid/reference/functions/useLiveQuery.md b/docs/framework/solid/reference/functions/useLiveQuery.md index 9237b7468d..39421244bc 100644 --- a/docs/framework/solid/reference/functions/useLiveQuery.md +++ b/docs/framework/solid/reference/functions/useLiveQuery.md @@ -223,7 +223,7 @@ Create a live query using configuration object #### config -`Accessor`\<`LiveQueryCollectionConfig`\<`TContext`, `RootQueryResult`\<`TContext`\>\>\> +`Accessor`\<`LiveQueryHookConfig`\<`TContext`\>\> Configuration object with query and options diff --git a/docs/framework/svelte/reference/type-aliases/UseLiveQueryConfig.md b/docs/framework/svelte/reference/type-aliases/UseLiveQueryConfig.md index 4c1468d9a8..ea5122de38 100644 --- a/docs/framework/svelte/reference/type-aliases/UseLiveQueryConfig.md +++ b/docs/framework/svelte/reference/type-aliases/UseLiveQueryConfig.md @@ -4,7 +4,7 @@ title: UseLiveQueryConfig --- ```ts -type UseLiveQueryConfig = LiveQueryCollectionConfig & object; +type UseLiveQueryConfig = LiveQueryHookConfig & object; ``` Defined in: [packages/svelte-db/src/useLiveQuery.svelte.ts:108](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L108) diff --git a/docs/framework/vue/reference/functions/useLiveQuery.md b/docs/framework/vue/reference/functions/useLiveQuery.md index 9f88603305..21aea1fe6e 100644 --- a/docs/framework/vue/reference/functions/useLiveQuery.md +++ b/docs/framework/vue/reference/functions/useLiveQuery.md @@ -195,7 +195,7 @@ Create a live query using configuration object #### config -`LiveQueryCollectionConfig`\<`TContext`\> +`LiveQueryHookConfig`\<`TContext`\> Configuration object with query and options diff --git a/docs/overview.md b/docs/overview.md index cd63288750..fb3e727997 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -323,7 +323,7 @@ You can also query across collections with joins: ```ts import { useLiveQuery } from '@tanstack/react-db' -import { eq } from '@tanstack/db' +import { eq, innerJoin } from '@tanstack/db' const Todos = () => { const { data: todos } = useLiveQuery({ diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md b/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md index 327fc833c5..7583c23076 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md @@ -3,7 +3,7 @@ id: Aggregate title: Aggregate --- -Defined in: [packages/db/src/query/ir.ts:274](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L274) +Defined in: [packages/db/src/query/ir.ts:269](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L269) ## Extends @@ -26,7 +26,7 @@ new Aggregate( grouping): Aggregate; ``` -Defined in: [packages/db/src/query/ir.ts:276](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L276) +Defined in: [packages/db/src/query/ir.ts:271](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L271) #### Parameters @@ -66,7 +66,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) **`Internal`** @@ -86,7 +86,7 @@ BaseExpression.__returnType args: BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:278](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L278) +Defined in: [packages/db/src/query/ir.ts:273](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L273) *** @@ -96,7 +96,7 @@ Defined in: [packages/db/src/query/ir.ts:278](https://github.com/TanStack/db/blo grouping: object; ``` -Defined in: [packages/db/src/query/ir.ts:280](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L280) +Defined in: [packages/db/src/query/ir.ts:275](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L275) #### processGroupBy() @@ -203,7 +203,7 @@ A transformed BasicExpression that references computed values instead of raw exp name: string; ``` -Defined in: [packages/db/src/query/ir.ts:277](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L277) +Defined in: [packages/db/src/query/ir.ts:272](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L272) *** @@ -213,7 +213,7 @@ Defined in: [packages/db/src/query/ir.ts:277](https://github.com/TanStack/db/blo type: "agg"; ``` -Defined in: [packages/db/src/query/ir.ts:275](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L275) +Defined in: [packages/db/src/query/ir.ts:270](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L270) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md b/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md index 6340a15ded..b6beb43436 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md @@ -3,7 +3,7 @@ id: CollectionRef title: CollectionRef --- -Defined in: [packages/db/src/query/ir.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L100) +Defined in: [packages/db/src/query/ir.ts:95](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L95) ## Extends @@ -20,7 +20,7 @@ new CollectionRef( bindingId?): CollectionRef; ``` -Defined in: [packages/db/src/query/ir.ts:107](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L107) +Defined in: [packages/db/src/query/ir.ts:102](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L102) #### Parameters @@ -54,7 +54,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) **`Internal`** @@ -74,7 +74,7 @@ BaseExpression.__returnType alias: string; ``` -Defined in: [packages/db/src/query/ir.ts:105](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L105) +Defined in: [packages/db/src/query/ir.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L100) *** @@ -84,7 +84,7 @@ Defined in: [packages/db/src/query/ir.ts:105](https://github.com/TanStack/db/blo collection: CollectionImpl; ``` -Defined in: [packages/db/src/query/ir.ts:104](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L104) +Defined in: [packages/db/src/query/ir.ts:99](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L99) *** @@ -94,7 +94,7 @@ Defined in: [packages/db/src/query/ir.ts:104](https://github.com/TanStack/db/blo type: "collectionRef"; ``` -Defined in: [packages/db/src/query/ir.ts:101](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L101) +Defined in: [packages/db/src/query/ir.ts:96](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L96) #### Overrides @@ -112,7 +112,7 @@ BaseExpression.type get bindingId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:123](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L123) +Defined in: [packages/db/src/query/ir.ts:118](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L118) ##### Returns @@ -128,7 +128,7 @@ Defined in: [packages/db/src/query/ir.ts:123](https://github.com/TanStack/db/blo get sourceId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:119](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L119) +Defined in: [packages/db/src/query/ir.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L114) Opaque runtime identity; aliases are lexical names only. diff --git a/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md b/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md index d5ba64669d..48d6ab444b 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md @@ -3,7 +3,7 @@ id: ConditionalSelect title: ConditionalSelect --- -Defined in: [packages/db/src/query/ir.ts:322](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L322) +Defined in: [packages/db/src/query/ir.ts:317](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L317) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:322](https://github.com/TanStack/db/blo new ConditionalSelect(branches, defaultValue?): ConditionalSelect; ``` -Defined in: [packages/db/src/query/ir.ts:324](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L324) +Defined in: [packages/db/src/query/ir.ts:319](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L319) #### Parameters @@ -47,7 +47,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) **`Internal`** @@ -67,7 +67,7 @@ BaseExpression.__returnType branches: ConditionalSelectBranch[]; ``` -Defined in: [packages/db/src/query/ir.ts:325](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L325) +Defined in: [packages/db/src/query/ir.ts:320](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L320) *** @@ -77,7 +77,7 @@ Defined in: [packages/db/src/query/ir.ts:325](https://github.com/TanStack/db/blo optional defaultValue: SelectValueExpression; ``` -Defined in: [packages/db/src/query/ir.ts:326](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L326) +Defined in: [packages/db/src/query/ir.ts:321](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L321) *** @@ -87,7 +87,7 @@ Defined in: [packages/db/src/query/ir.ts:326](https://github.com/TanStack/db/blo type: "conditionalSelect"; ``` -Defined in: [packages/db/src/query/ir.ts:323](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L323) +Defined in: [packages/db/src/query/ir.ts:318](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L318) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md b/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md index ab17414d06..02d169b3ac 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md @@ -3,7 +3,7 @@ id: DescriptorRef title: DescriptorRef --- -Defined in: [packages/db/src/query/ir.ts:128](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L128) +Defined in: [packages/db/src/query/ir.ts:123](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L123) ## Extends @@ -20,7 +20,7 @@ new DescriptorRef( bindingId?): DescriptorRef; ``` -Defined in: [packages/db/src/query/ir.ts:132](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L132) +Defined in: [packages/db/src/query/ir.ts:127](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L127) #### Parameters @@ -54,7 +54,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) **`Internal`** @@ -74,7 +74,7 @@ BaseExpression.__returnType alias: string; ``` -Defined in: [packages/db/src/query/ir.ts:134](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L134) +Defined in: [packages/db/src/query/ir.ts:129](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L129) *** @@ -84,7 +84,7 @@ Defined in: [packages/db/src/query/ir.ts:134](https://github.com/TanStack/db/blo descriptor: CollectionOptionsIdentity; ``` -Defined in: [packages/db/src/query/ir.ts:133](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L133) +Defined in: [packages/db/src/query/ir.ts:128](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L128) *** @@ -94,7 +94,7 @@ Defined in: [packages/db/src/query/ir.ts:133](https://github.com/TanStack/db/blo type: "descriptorRef"; ``` -Defined in: [packages/db/src/query/ir.ts:129](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L129) +Defined in: [packages/db/src/query/ir.ts:124](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L124) #### Overrides @@ -112,7 +112,7 @@ BaseExpression.type get bindingId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:146](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L146) +Defined in: [packages/db/src/query/ir.ts:141](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L141) ##### Returns @@ -128,7 +128,7 @@ Defined in: [packages/db/src/query/ir.ts:146](https://github.com/TanStack/db/blo get sourceId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:142](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L142) +Defined in: [packages/db/src/query/ir.ts:137](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L137) Opaque runtime identity for this unbound source placement. diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Func.md b/docs/reference/@tanstack/namespaces/IR/classes/Func.md index 5f0192d7df..485e4bc2f4 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Func.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Func.md @@ -3,7 +3,7 @@ id: Func title: Func --- -Defined in: [packages/db/src/query/ir.ts:259](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L259) +Defined in: [packages/db/src/query/ir.ts:254](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L254) ## Extends @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/ir.ts:259](https://github.com/TanStack/db/blo new Func(name, args): Func; ``` -Defined in: [packages/db/src/query/ir.ts:261](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L261) +Defined in: [packages/db/src/query/ir.ts:256](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L256) #### Parameters @@ -53,7 +53,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) **`Internal`** @@ -73,7 +73,7 @@ BaseExpression.__returnType args: BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:263](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L263) +Defined in: [packages/db/src/query/ir.ts:258](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L258) *** @@ -83,7 +83,7 @@ Defined in: [packages/db/src/query/ir.ts:263](https://github.com/TanStack/db/blo name: string; ``` -Defined in: [packages/db/src/query/ir.ts:262](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L262) +Defined in: [packages/db/src/query/ir.ts:257](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L257) *** @@ -93,7 +93,7 @@ Defined in: [packages/db/src/query/ir.ts:262](https://github.com/TanStack/db/blo type: "func"; ``` -Defined in: [packages/db/src/query/ir.ts:260](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L260) +Defined in: [packages/db/src/query/ir.ts:255](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L255) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md b/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md index 315f4695c6..21c4001d72 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md @@ -3,7 +3,7 @@ id: IncludesSubquery title: IncludesSubquery --- -Defined in: [packages/db/src/query/ir.ts:296](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L296) +Defined in: [packages/db/src/query/ir.ts:291](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L291) ## Extends @@ -26,7 +26,7 @@ new IncludesSubquery( scalarField?): IncludesSubquery; ``` -Defined in: [packages/db/src/query/ir.ts:298](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L298) +Defined in: [packages/db/src/query/ir.ts:293](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L293) #### Parameters @@ -90,7 +90,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) **`Internal`** @@ -110,7 +110,7 @@ BaseExpression.__returnType childCorrelationField: PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:301](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L301) +Defined in: [packages/db/src/query/ir.ts:296](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L296) *** @@ -120,7 +120,7 @@ Defined in: [packages/db/src/query/ir.ts:301](https://github.com/TanStack/db/blo correlationField: PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:300](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L300) +Defined in: [packages/db/src/query/ir.ts:295](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L295) *** @@ -130,7 +130,7 @@ Defined in: [packages/db/src/query/ir.ts:300](https://github.com/TanStack/db/blo fieldName: string; ``` -Defined in: [packages/db/src/query/ir.ts:302](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L302) +Defined in: [packages/db/src/query/ir.ts:297](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L297) *** @@ -140,7 +140,7 @@ Defined in: [packages/db/src/query/ir.ts:302](https://github.com/TanStack/db/blo materialization: IncludesMaterialization; ``` -Defined in: [packages/db/src/query/ir.ts:307](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L307) +Defined in: [packages/db/src/query/ir.ts:302](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L302) *** @@ -150,7 +150,7 @@ Defined in: [packages/db/src/query/ir.ts:307](https://github.com/TanStack/db/blo optional parentFilters: Where[]; ``` -Defined in: [packages/db/src/query/ir.ts:305](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L305) +Defined in: [packages/db/src/query/ir.ts:300](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L300) *** @@ -160,7 +160,7 @@ Defined in: [packages/db/src/query/ir.ts:305](https://github.com/TanStack/db/blo optional parentProjection: PropRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:306](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L306) +Defined in: [packages/db/src/query/ir.ts:301](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L301) *** @@ -170,7 +170,7 @@ Defined in: [packages/db/src/query/ir.ts:306](https://github.com/TanStack/db/blo query: QueryIR; ``` -Defined in: [packages/db/src/query/ir.ts:299](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L299) +Defined in: [packages/db/src/query/ir.ts:294](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L294) *** @@ -180,7 +180,7 @@ Defined in: [packages/db/src/query/ir.ts:299](https://github.com/TanStack/db/blo runtime: object; ``` -Defined in: [packages/db/src/query/ir.ts:304](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L304) +Defined in: [packages/db/src/query/ir.ts:299](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L299) #### createBucketFacades() @@ -234,7 +234,7 @@ adapter resolves those references after the graph reaches quiescence. optional scalarField: string; ``` -Defined in: [packages/db/src/query/ir.ts:308](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L308) +Defined in: [packages/db/src/query/ir.ts:303](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L303) *** @@ -244,7 +244,7 @@ Defined in: [packages/db/src/query/ir.ts:308](https://github.com/TanStack/db/blo type: "includesSubquery"; ``` -Defined in: [packages/db/src/query/ir.ts:297](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L297) +Defined in: [packages/db/src/query/ir.ts:292](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L292) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md b/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md index 4c374e2288..d5716c4865 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md @@ -3,7 +3,7 @@ id: PropRef title: PropRef --- -Defined in: [packages/db/src/query/ir.ts:214](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L214) +Defined in: [packages/db/src/query/ir.ts:209](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L209) ## Extends @@ -26,7 +26,7 @@ new PropRef( bindingId?): PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:218](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L218) +Defined in: [packages/db/src/query/ir.ts:213](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L213) #### Parameters @@ -60,7 +60,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) **`Internal`** @@ -80,7 +80,7 @@ BaseExpression.__returnType readonly optional bindingId: string; ``` -Defined in: [packages/db/src/query/ir.ts:217](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L217) +Defined in: [packages/db/src/query/ir.ts:212](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L212) *** @@ -90,7 +90,7 @@ Defined in: [packages/db/src/query/ir.ts:217](https://github.com/TanStack/db/blo path: string[]; ``` -Defined in: [packages/db/src/query/ir.ts:219](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L219) +Defined in: [packages/db/src/query/ir.ts:214](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L214) *** @@ -100,7 +100,7 @@ Defined in: [packages/db/src/query/ir.ts:219](https://github.com/TanStack/db/blo readonly optional sourceAlias: string; ``` -Defined in: [packages/db/src/query/ir.ts:216](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L216) +Defined in: [packages/db/src/query/ir.ts:211](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L211) *** @@ -110,7 +110,7 @@ Defined in: [packages/db/src/query/ir.ts:216](https://github.com/TanStack/db/blo type: "ref"; ``` -Defined in: [packages/db/src/query/ir.ts:215](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L215) +Defined in: [packages/db/src/query/ir.ts:210](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L210) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md b/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md index 78100fc3e9..1e8d2f148c 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md @@ -3,7 +3,7 @@ id: QueryRef title: QueryRef --- -Defined in: [packages/db/src/query/ir.ts:169](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L169) +Defined in: [packages/db/src/query/ir.ts:164](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L164) ## Extends @@ -20,7 +20,7 @@ new QueryRef( bindingId?): QueryRef; ``` -Defined in: [packages/db/src/query/ir.ts:172](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L172) +Defined in: [packages/db/src/query/ir.ts:167](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L167) #### Parameters @@ -54,7 +54,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) **`Internal`** @@ -74,7 +74,7 @@ BaseExpression.__returnType alias: string; ``` -Defined in: [packages/db/src/query/ir.ts:174](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L174) +Defined in: [packages/db/src/query/ir.ts:169](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L169) *** @@ -84,7 +84,7 @@ Defined in: [packages/db/src/query/ir.ts:174](https://github.com/TanStack/db/blo query: QueryIR; ``` -Defined in: [packages/db/src/query/ir.ts:173](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L173) +Defined in: [packages/db/src/query/ir.ts:168](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L168) *** @@ -94,7 +94,7 @@ Defined in: [packages/db/src/query/ir.ts:173](https://github.com/TanStack/db/blo type: "queryRef"; ``` -Defined in: [packages/db/src/query/ir.ts:170](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L170) +Defined in: [packages/db/src/query/ir.ts:165](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L165) #### Overrides @@ -112,7 +112,7 @@ BaseExpression.type get bindingId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:181](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L181) +Defined in: [packages/db/src/query/ir.ts:176](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L176) ##### Returns diff --git a/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md b/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md index 94597bc287..36f077a7aa 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md @@ -3,7 +3,7 @@ id: UnionAll title: UnionAll --- -Defined in: [packages/db/src/query/ir.ts:197](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L197) +Defined in: [packages/db/src/query/ir.ts:192](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L192) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:197](https://github.com/TanStack/db/blo new UnionAll(queries): UnionAll; ``` -Defined in: [packages/db/src/query/ir.ts:205](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L205) +Defined in: [packages/db/src/query/ir.ts:200](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L200) Result-level UNION ALL. Downstream query clauses see the union result row shape, not the branch source aliases. Optimizers may push safe operations @@ -48,7 +48,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) **`Internal`** @@ -68,7 +68,7 @@ BaseExpression.__returnType queries: QueryIR[]; ``` -Defined in: [packages/db/src/query/ir.ts:205](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L205) +Defined in: [packages/db/src/query/ir.ts:200](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L200) *** @@ -78,7 +78,7 @@ Defined in: [packages/db/src/query/ir.ts:205](https://github.com/TanStack/db/blo type: "unionAll"; ``` -Defined in: [packages/db/src/query/ir.ts:198](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L198) +Defined in: [packages/db/src/query/ir.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L193) #### Overrides @@ -96,7 +96,7 @@ BaseExpression.type get alias(): string; ``` -Defined in: [packages/db/src/query/ir.ts:209](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L209) +Defined in: [packages/db/src/query/ir.ts:204](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L204) ##### Returns diff --git a/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md b/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md index c5a3aac729..914bae09c9 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md @@ -3,7 +3,7 @@ id: UnionFrom title: UnionFrom --- -Defined in: [packages/db/src/query/ir.ts:186](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L186) +Defined in: [packages/db/src/query/ir.ts:181](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L181) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:186](https://github.com/TanStack/db/blo new UnionFrom(sources): UnionFrom; ``` -Defined in: [packages/db/src/query/ir.ts:188](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L188) +Defined in: [packages/db/src/query/ir.ts:183](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L183) #### Parameters @@ -45,7 +45,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) **`Internal`** @@ -67,7 +67,7 @@ sources: ( | QueryRef)[]; ``` -Defined in: [packages/db/src/query/ir.ts:188](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L188) +Defined in: [packages/db/src/query/ir.ts:183](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L183) *** @@ -77,7 +77,7 @@ Defined in: [packages/db/src/query/ir.ts:188](https://github.com/TanStack/db/blo type: "unionFrom"; ``` -Defined in: [packages/db/src/query/ir.ts:187](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L187) +Defined in: [packages/db/src/query/ir.ts:182](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L182) #### Overrides @@ -95,7 +95,7 @@ BaseExpression.type get alias(): string; ``` -Defined in: [packages/db/src/query/ir.ts:192](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L192) +Defined in: [packages/db/src/query/ir.ts:187](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L187) ##### Returns diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Value.md b/docs/reference/@tanstack/namespaces/IR/classes/Value.md index 60029193cf..cec645caa4 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Value.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Value.md @@ -3,7 +3,7 @@ id: Value title: Value --- -Defined in: [packages/db/src/query/ir.ts:250](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L250) +Defined in: [packages/db/src/query/ir.ts:245](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L245) ## Extends @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/ir.ts:250](https://github.com/TanStack/db/blo new Value(value): Value; ``` -Defined in: [packages/db/src/query/ir.ts:252](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L252) +Defined in: [packages/db/src/query/ir.ts:247](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L247) #### Parameters @@ -49,7 +49,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) **`Internal`** @@ -69,7 +69,7 @@ BaseExpression.__returnType type: "val"; ``` -Defined in: [packages/db/src/query/ir.ts:251](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L251) +Defined in: [packages/db/src/query/ir.ts:246](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L246) #### Overrides @@ -85,4 +85,4 @@ BaseExpression.type value: T; ``` -Defined in: [packages/db/src/query/ir.ts:253](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L253) +Defined in: [packages/db/src/query/ir.ts:248](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L248) diff --git a/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md b/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md index 1f31eae7df..ae39f68b65 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md @@ -7,7 +7,7 @@ title: collectCollectionSources function collectCollectionSources(query): CollectionRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:402](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L402) +Defined in: [packages/db/src/query/ir.ts:397](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L397) Returns concrete Collection sources after requiring client binding. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md b/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md index e74e70c870..69dbfd1e78 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md @@ -7,7 +7,7 @@ title: collectPropRefs function collectPropRefs(expression): PropRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:287](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L287) +Defined in: [packages/db/src/query/ir.ts:282](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L282) Collect refs from an IR expression, including aggregate arguments. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md b/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md index b9cc577395..312204d6f6 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md @@ -7,7 +7,7 @@ title: collectSourceRefs function collectSourceRefs(query): CollectionSourceRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:353](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L353) +Defined in: [packages/db/src/query/ir.ts:348](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L348) Returns each lexical Collection or descriptor source in a query tree once. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md b/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md index c5fd381f4b..ecffa6f96d 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md @@ -7,7 +7,7 @@ title: createResidualWhere function createResidualWhere(expression): Where; ``` -Defined in: [packages/db/src/query/ir.ts:447](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L447) +Defined in: [packages/db/src/query/ir.ts:442](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L442) Create a residual Where clause from an expression diff --git a/docs/reference/@tanstack/namespaces/IR/functions/followRef.md b/docs/reference/@tanstack/namespaces/IR/functions/followRef.md index 64340b8a4e..8857836edb 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/followRef.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/followRef.md @@ -17,7 +17,7 @@ function followRef( }; ``` -Defined in: [packages/db/src/query/ir.ts:495](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L495) +Defined in: [packages/db/src/query/ir.ts:490](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L490) Follows the given reference in a query until its finds the root field the reference points to. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md b/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md index d31d0d432e..3b54e2ef80 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md @@ -9,7 +9,7 @@ function getFromSources(from): ( | QueryRef)[]; ``` -Defined in: [packages/db/src/query/ir.ts:454](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L454) +Defined in: [packages/db/src/query/ir.ts:449](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L449) Sources declared by a FROM clause. UnionAll branches own their sources. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md b/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md index 5c5ec25f26..c3e407e73f 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md @@ -9,7 +9,7 @@ function getHavingExpression(having): | Aggregate; ``` -Defined in: [packages/db/src/query/ir.ts:425](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L425) +Defined in: [packages/db/src/query/ir.ts:420](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L420) Extract the expression from a HAVING clause HAVING clauses can contain aggregates, unlike regular WHERE clauses diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md index 556f77b80e..097ec72d56 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md @@ -7,7 +7,7 @@ title: getPropRefPropertyPath function getPropRefPropertyPath(ref): string[]; ``` -Defined in: [packages/db/src/query/ir.ts:246](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L246) +Defined in: [packages/db/src/query/ir.ts:241](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L241) Returns the property path after removing only explicit source qualification. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md index 6cc8981db0..903557d8aa 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md @@ -7,7 +7,7 @@ title: getPropRefSourceAlias function getPropRefSourceAlias(ref): string | undefined; ``` -Defined in: [packages/db/src/query/ir.ts:239](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L239) +Defined in: [packages/db/src/query/ir.ts:234](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L234) Returns an explicitly declared source alias without inferring from the path. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md b/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md index 773bff4a40..1838abc848 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md @@ -7,7 +7,7 @@ title: getWhereExpression function getWhereExpression(where): BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:415](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L415) +Defined in: [packages/db/src/query/ir.ts:410](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L410) Extract the expression from a Where clause diff --git a/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md b/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md index 2b3ffc0c0a..fc18c6038e 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md @@ -7,7 +7,7 @@ title: isBasicOrAggregateExpression function isBasicOrAggregateExpression(value): value is BasicExpression | Aggregate; ``` -Defined in: [packages/db/src/query/ir.ts:333](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L333) +Defined in: [packages/db/src/query/ir.ts:328](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L328) Distinguish compiler expressions from user objects with IR-like fields. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md b/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md index d5fc5d8c1b..86e60c410a 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md @@ -7,7 +7,7 @@ title: isExpressionLike function isExpressionLike(value): boolean; ``` -Defined in: [packages/db/src/query/ir.ts:344](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L344) +Defined in: [packages/db/src/query/ir.ts:339](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L339) ## Parameters diff --git a/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md b/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md index 1ebfef7204..00a808222c 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md @@ -7,7 +7,7 @@ title: isResidualWhere function isResidualWhere(where): boolean; ``` -Defined in: [packages/db/src/query/ir.ts:436](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L436) +Defined in: [packages/db/src/query/ir.ts:431](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L431) Check if a Where clause is marked as residual diff --git a/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md b/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md index 01a9961fcc..6faaf4f1d0 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md @@ -9,7 +9,7 @@ title: requireCollectionSource function requireCollectionSource(source): never; ``` -Defined in: [packages/db/src/query/ir.ts:152](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L152) +Defined in: [packages/db/src/query/ir.ts:147](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L147) A descriptor must bind to a DbClient before a Collection is required. @@ -29,7 +29,7 @@ A descriptor must bind to a DbClient before a Collection is required. function requireCollectionSource(source): CollectionImpl; ``` -Defined in: [packages/db/src/query/ir.ts:153](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L153) +Defined in: [packages/db/src/query/ir.ts:148](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L148) A descriptor must bind to a DbClient before a Collection is required. diff --git a/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md b/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md index 0e67e96bd1..d6002e6765 100644 --- a/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md +++ b/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md @@ -3,208 +3,230 @@ id: JoinClause title: JoinClause --- -Defined in: [packages/db/src/query/ir.ts:56](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L56) +Defined in: [packages/db/src/query/ir.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L54) ## Properties -### compile() +### from + +```ts +from: + | CollectionSourceRef + | QueryRef; +``` + +Defined in: [packages/db/src/query/ir.ts:55](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L55) + +*** + +### on ```ts -compile: (pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, localFrom, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, valueIdentity, parentKeyStream?) => NamespacedAndKeyedStream; +on: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:62](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L62) +Defined in: [packages/db/src/query/ir.ts:57](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L57) + +*** + +### runtime + +```ts +runtime: object; +``` + +Defined in: [packages/db/src/query/ir.ts:60](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L60) + +#### compile() + +```ts +compile: (pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, localFrom, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, valueIdentity, parentKeyStream?) => NamespacedAndKeyedStream = processJoins; +``` Processes all join clauses, applying lazy loading optimizations and maintaining alias tracking for per-alias subscriptions (enables self-joins). -#### Parameters +##### Parameters -##### pipeline +###### pipeline [`NamespacedAndKeyedStream`](../../../../type-aliases/NamespacedAndKeyedStream.md) -##### joinClauses +###### joinClauses `JoinClause`[] -##### sources +###### sources `Record`\<`string`, [`KeyedStream`](../../../../type-aliases/KeyedStream.md)\> -##### mainCollectionId +###### mainCollectionId `string` -##### mainSource +###### mainSource `string` -##### allInputs +###### allInputs `Record`\<`string`, [`KeyedStream`](../../../../type-aliases/KeyedStream.md)\> -##### cache +###### cache `QueryCache` -##### queryMapping +###### queryMapping `QueryMapping` -##### collections +###### collections `Record`\<`string`, [`Collection`](../../../../interfaces/Collection.md)\> -##### subscriptions +###### subscriptions `Record`\<`string`, `CollectionSubscription`\> -##### callbacks +###### callbacks `Record`\<`string`, `LazyCollectionCallbacks`\> -##### lazySources +###### lazySources `Set`\<`string`\> -##### optimizableOrderByCollections +###### optimizableOrderByCollections `Record`\<`string`, `OrderByOptimizationInfo`\> -##### setWindowFn +###### setWindowFn (`windowFn`) => `void` -##### rawQuery +###### rawQuery [`QueryIR`](QueryIR.md) -##### localFrom +###### localFrom [`From`](../type-aliases/From.md) -##### onCompileSubquery +###### onCompileSubquery (...`args`) => `CompilationResult` -##### aliasToCollectionId +###### aliasToCollectionId `Record`\<`string`, `string`\> -##### aliasRemapping +###### aliasRemapping `Record`\<`string`, `string`\> -##### sourceWhereClauses +###### sourceWhereClauses `Map`\<`string`, [`BasicExpression`](../type-aliases/BasicExpression.md)\<`boolean`\>\> -##### mainSourceIsParentFiltered +###### mainSourceIsParentFiltered `boolean` -##### valueIdentity +###### valueIdentity `ValueIdentity` -##### parentKeyStream? +###### parentKeyStream? [`KeyedStream`](../../../../type-aliases/KeyedStream.md) -#### Returns +##### Returns [`NamespacedAndKeyedStream`](../../../../type-aliases/NamespacedAndKeyedStream.md) -*** - -### from +#### optimize() ```ts -from: - | CollectionSourceRef - | QueryRef; +optimize: (query, groupedClauses) => QueryIR = applyOptimizations; ``` -Defined in: [packages/db/src/query/ir.ts:57](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L57) - -*** +Step 4: Apply optimizations by lifting single-source clauses into subqueries. -### materialize() +Creates a new QueryIR with single-source WHERE clauses moved to subqueries +that wrap the original table references. This ensures immutability and prevents +infinite recursion issues. -```ts -materialize: (compilation, getRootKey?, reduceJoinedPublicKeys) => MaterializedRootCompilation; -``` +##### Parameters -Defined in: [packages/db/src/query/ir.ts:65](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L65) +###### query -Compiles inline includes into the same D2 graph as their parent relation. -Collection-valued includes become inert bucket references. The public facade -adapter resolves those references after the graph reaches quiescence. +[`QueryIR`](QueryIR.md) -#### Parameters +Original QueryIR to optimize -##### compilation +###### groupedClauses -`CompilationResult` +`GroupedWhereClauses` -##### getRootKey? +WHERE clauses grouped by optimization strategy -(`row`) => `unknown` +##### Returns -##### reduceJoinedPublicKeys? +[`QueryIR`](QueryIR.md) -`boolean` = `false` +New QueryIR with optimizations applied -#### Returns +#### reducePublicKeys() -`MaterializedRootCompilation` +```ts +reducePublicKeys: (compilation, getKey) => object; +``` -*** +Reduces a query without includes to one canonical row per public key. -### on +##### Parameters -```ts -on: BasicExpression; -``` +###### compilation -Defined in: [packages/db/src/query/ir.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L59) +`CompilationResult` -*** +###### getKey -### optimize() +(`row`) => `unknown` -```ts -optimize: (query, groupedClauses) => QueryIR; -``` +##### Returns -Defined in: [packages/db/src/query/ir.ts:63](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L63) +`object` -Step 4: Apply optimizations by lifting single-source clauses into subqueries. +###### facades -Creates a new QueryIR with single-source WHERE clauses moved to subqueries -that wrap the original table references. This ensures immutability and prevents -infinite recursion issues. +```ts +facades: undefined; +``` -#### Parameters +###### pipeline -##### query +```ts +pipeline: ResultStream; +``` -[`QueryIR`](QueryIR.md) +#### validate() -Original QueryIR to optimize +```ts +validate: (expression) => readonly [BasicExpression, BasicExpression][] = validateJoinConditions; +``` -##### groupedClauses +Validate join syntax and return its equality operands in source order. -`GroupedWhereClauses` +##### Parameters -WHERE clauses grouped by optimization strategy +###### expression -#### Returns +[`BasicExpression`](../type-aliases/BasicExpression.md) -[`QueryIR`](QueryIR.md) +##### Returns -New QueryIR with optimizations applied +readonly \[[`BasicExpression`](../type-aliases/BasicExpression.md), [`BasicExpression`](../type-aliases/BasicExpression.md)\][] *** @@ -214,4 +236,4 @@ New QueryIR with optimizations applied type: "inner" | "left" | "right" | "full" | "outer" | "cross"; ``` -Defined in: [packages/db/src/query/ir.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L58) +Defined in: [packages/db/src/query/ir.ts:56](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L56) diff --git a/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md b/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md index 08b31d931c..2fabad5f13 100644 --- a/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md +++ b/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md @@ -3,7 +3,7 @@ id: QueryIR title: QueryIR --- -Defined in: [packages/db/src/query/ir.ts:19](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L19) +Defined in: [packages/db/src/query/ir.ts:17](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L17) ## Properties @@ -13,7 +13,7 @@ Defined in: [packages/db/src/query/ir.ts:19](https://github.com/TanStack/db/blob optional distinct: true; ``` -Defined in: [packages/db/src/query/ir.ts:31](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L31) +Defined in: [packages/db/src/query/ir.ts:29](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L29) *** @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/ir.ts:31](https://github.com/TanStack/db/blob optional fnHaving: (row) => any[]; ``` -Defined in: [packages/db/src/query/ir.ts:37](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L37) +Defined in: [packages/db/src/query/ir.ts:35](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L35) #### Parameters @@ -43,7 +43,7 @@ Defined in: [packages/db/src/query/ir.ts:37](https://github.com/TanStack/db/blob optional fnSelect: (row) => any; ``` -Defined in: [packages/db/src/query/ir.ts:35](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L35) +Defined in: [packages/db/src/query/ir.ts:33](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L33) #### Parameters @@ -63,7 +63,7 @@ Defined in: [packages/db/src/query/ir.ts:35](https://github.com/TanStack/db/blob optional fnWhere: (row) => any[]; ``` -Defined in: [packages/db/src/query/ir.ts:36](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L36) +Defined in: [packages/db/src/query/ir.ts:34](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L34) #### Parameters @@ -83,7 +83,7 @@ Defined in: [packages/db/src/query/ir.ts:36](https://github.com/TanStack/db/blob from: From; ``` -Defined in: [packages/db/src/query/ir.ts:20](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L20) +Defined in: [packages/db/src/query/ir.ts:18](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L18) *** @@ -93,7 +93,7 @@ Defined in: [packages/db/src/query/ir.ts:20](https://github.com/TanStack/db/blob optional groupBy: GroupBy; ``` -Defined in: [packages/db/src/query/ir.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L24) +Defined in: [packages/db/src/query/ir.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L22) *** @@ -103,7 +103,7 @@ Defined in: [packages/db/src/query/ir.ts:24](https://github.com/TanStack/db/blob optional grouping: object; ``` -Defined in: [packages/db/src/query/ir.ts:26](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L26) +Defined in: [packages/db/src/query/ir.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L24) #### processGroupBy() @@ -210,7 +210,7 @@ A transformed BasicExpression that references computed values instead of raw exp optional having: Where[]; ``` -Defined in: [packages/db/src/query/ir.ts:27](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L27) +Defined in: [packages/db/src/query/ir.ts:25](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L25) *** @@ -220,7 +220,7 @@ Defined in: [packages/db/src/query/ir.ts:27](https://github.com/TanStack/db/blob optional join: Join; ``` -Defined in: [packages/db/src/query/ir.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L22) +Defined in: [packages/db/src/query/ir.ts:20](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L20) *** @@ -230,7 +230,7 @@ Defined in: [packages/db/src/query/ir.ts:22](https://github.com/TanStack/db/blob optional limit: number; ``` -Defined in: [packages/db/src/query/ir.ts:29](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L29) +Defined in: [packages/db/src/query/ir.ts:27](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L27) *** @@ -240,7 +240,7 @@ Defined in: [packages/db/src/query/ir.ts:29](https://github.com/TanStack/db/blob optional offset: number; ``` -Defined in: [packages/db/src/query/ir.ts:30](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L30) +Defined in: [packages/db/src/query/ir.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L28) *** @@ -250,7 +250,7 @@ Defined in: [packages/db/src/query/ir.ts:30](https://github.com/TanStack/db/blob optional orderBy: OrderBy; ``` -Defined in: [packages/db/src/query/ir.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L28) +Defined in: [packages/db/src/query/ir.ts:26](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L26) *** @@ -260,7 +260,7 @@ Defined in: [packages/db/src/query/ir.ts:28](https://github.com/TanStack/db/blob optional select: Select; ``` -Defined in: [packages/db/src/query/ir.ts:21](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L21) +Defined in: [packages/db/src/query/ir.ts:19](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L19) *** @@ -270,7 +270,7 @@ Defined in: [packages/db/src/query/ir.ts:21](https://github.com/TanStack/db/blob optional singleResult: true; ``` -Defined in: [packages/db/src/query/ir.ts:32](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L32) +Defined in: [packages/db/src/query/ir.ts:30](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L30) *** @@ -280,4 +280,4 @@ Defined in: [packages/db/src/query/ir.ts:32](https://github.com/TanStack/db/blob optional where: Where[]; ``` -Defined in: [packages/db/src/query/ir.ts:23](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L23) +Defined in: [packages/db/src/query/ir.ts:21](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L21) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md index c362542670..dda743a2b0 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md @@ -10,7 +10,7 @@ type BasicExpression = | Func; ``` -Defined in: [packages/db/src/query/ir.ts:272](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L272) +Defined in: [packages/db/src/query/ir.ts:267](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L267) ## Type Parameters diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md index 27a23d1865..cbe8ce0a66 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md @@ -9,4 +9,4 @@ type CollectionSourceRef = | DescriptorRef; ``` -Defined in: [packages/db/src/query/ir.ts:45](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L45) +Defined in: [packages/db/src/query/ir.ts:43](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L43) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md index 57444c9ab0..a0d467b5ec 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md @@ -7,7 +7,7 @@ title: ConditionalSelectBranch type ConditionalSelectBranch = object; ``` -Defined in: [packages/db/src/query/ir.ts:314](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L314) +Defined in: [packages/db/src/query/ir.ts:309](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L309) ## Properties @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:314](https://github.com/TanStack/db/blo condition: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:315](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L315) +Defined in: [packages/db/src/query/ir.ts:310](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L310) *** @@ -27,4 +27,4 @@ Defined in: [packages/db/src/query/ir.ts:315](https://github.com/TanStack/db/blo value: SelectValueExpression; ``` -Defined in: [packages/db/src/query/ir.ts:316](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L316) +Defined in: [packages/db/src/query/ir.ts:311](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L311) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md index 2a823b3686..b7ae114705 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md @@ -11,4 +11,4 @@ type From = | UnionAll; ``` -Defined in: [packages/db/src/query/ir.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L47) +Defined in: [packages/db/src/query/ir.ts:45](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L45) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md index 39fcb7dfae..3d56fd3164 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md @@ -7,4 +7,4 @@ title: GroupBy type GroupBy = BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L72) +Defined in: [packages/db/src/query/ir.ts:67](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L67) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md index e785f66252..66364095d6 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md @@ -7,4 +7,4 @@ title: Having type Having = Where; ``` -Defined in: [packages/db/src/query/ir.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L74) +Defined in: [packages/db/src/query/ir.ts:69](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L69) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md index 9493ebb62e..4bbd86b534 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md @@ -7,4 +7,4 @@ title: IncludesMaterialization type IncludesMaterialization = "collection" | "array" | "singleton" | "concat"; ``` -Defined in: [packages/db/src/query/ir.ts:40](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L40) +Defined in: [packages/db/src/query/ir.ts:38](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L38) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md index 098a1f2af4..9be18ed357 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md @@ -7,4 +7,4 @@ title: Join type Join = JoinClause[]; ``` -Defined in: [packages/db/src/query/ir.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L54) +Defined in: [packages/db/src/query/ir.ts:52](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L52) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md index ba7d92188d..47931bec6c 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md @@ -7,4 +7,4 @@ title: Limit type Limit = number; ``` -Defined in: [packages/db/src/query/ir.ts:85](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L85) +Defined in: [packages/db/src/query/ir.ts:80](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L80) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md index 1cd8beb575..180646548f 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md @@ -7,4 +7,4 @@ title: Offset type Offset = number; ``` -Defined in: [packages/db/src/query/ir.ts:87](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L87) +Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md index 73d4d8137f..05c7d71787 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md @@ -7,4 +7,4 @@ title: OrderBy type OrderBy = OrderByClause[]; ``` -Defined in: [packages/db/src/query/ir.ts:76](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L76) +Defined in: [packages/db/src/query/ir.ts:71](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L71) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md index f0be0829f4..357401ccac 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md @@ -7,7 +7,7 @@ title: OrderByClause type OrderByClause = object; ``` -Defined in: [packages/db/src/query/ir.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L78) +Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L73) ## Properties @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:78](https://github.com/TanStack/db/blob compareOptions: CompareOptions; ``` -Defined in: [packages/db/src/query/ir.ts:80](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L80) +Defined in: [packages/db/src/query/ir.ts:75](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L75) *** @@ -27,4 +27,4 @@ Defined in: [packages/db/src/query/ir.ts:80](https://github.com/TanStack/db/blob expression: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:79](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L79) +Defined in: [packages/db/src/query/ir.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L74) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md index 77d0dc3d6e..5a8bf7acb6 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md @@ -7,4 +7,4 @@ title: OrderByDirection type OrderByDirection = "asc" | "desc"; ``` -Defined in: [packages/db/src/query/ir.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L83) +Defined in: [packages/db/src/query/ir.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L78) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md index b801434c9e..022054e1f9 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md @@ -7,7 +7,7 @@ title: Select type Select = object; ``` -Defined in: [packages/db/src/query/ir.ts:49](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L49) +Defined in: [packages/db/src/query/ir.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L47) ## Index Signature diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md index 8cbebc9a43..ca66f79c98 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md @@ -12,4 +12,4 @@ type SelectValueExpression = | ConditionalSelect; ``` -Defined in: [packages/db/src/query/ir.ts:319](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L319) +Defined in: [packages/db/src/query/ir.ts:314](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L314) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md index 8baa8ea90e..9e2be908bd 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md @@ -12,4 +12,4 @@ type Where = }; ``` -Defined in: [packages/db/src/query/ir.ts:68](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L68) +Defined in: [packages/db/src/query/ir.ts:63](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L63) diff --git a/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md b/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md index 99b98a72c4..091cb99177 100644 --- a/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md +++ b/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md @@ -7,4 +7,4 @@ title: INCLUDES_SCALAR_FIELD const INCLUDES_SCALAR_FIELD: "__includes_scalar__"; ``` -Defined in: [packages/db/src/query/ir.ts:43](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L43) +Defined in: [packages/db/src/query/ir.ts:41](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L41) diff --git a/docs/reference/classes/BaseQueryBuilder.md b/docs/reference/classes/BaseQueryBuilder.md index df243b9da6..2d5e783f40 100644 --- a/docs/reference/classes/BaseQueryBuilder.md +++ b/docs/reference/classes/BaseQueryBuilder.md @@ -49,7 +49,7 @@ Whether the builder may keep `query` without copying it. get fn(): object; ``` -Defined in: [packages/db/src/query/builder/index.ts:862](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L862) +Defined in: [packages/db/src/query/builder/index.ts:871](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L871) Functional variants of the query builder These are imperative function that are called for ery row. @@ -192,7 +192,7 @@ query _bindCollectionSources(resolveCollection): BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:967](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L967) +Defined in: [packages/db/src/query/builder/index.ts:976](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L976) Bind this query for one consumer. Later descriptor clauses wait for another consumer. @@ -214,7 +214,7 @@ Bind this query for one consumer. Later descriptor clauses wait for another cons _getQuery(): QueryIR; ``` -Defined in: [packages/db/src/query/builder/index.ts:959](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L959) +Defined in: [packages/db/src/query/builder/index.ts:968](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L968) #### Returns @@ -228,7 +228,7 @@ Defined in: [packages/db/src/query/builder/index.ts:959](https://github.com/TanS distinct(): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:779](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L779) +Defined in: [packages/db/src/query/builder/index.ts:788](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L788) Specify that the query should return distinct rows. Deduplicates rows based on the selected columns. @@ -257,7 +257,7 @@ query findOne(): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:799](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L799) +Defined in: [packages/db/src/query/builder/index.ts:808](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L808) Specify that the query should return a single result @@ -328,7 +328,7 @@ query.from({ activeUsers }) groupBy(keys): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:694](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L694) +Defined in: [packages/db/src/query/builder/index.ts:703](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L703) Group rows by one or more columns for aggregation @@ -336,6 +336,10 @@ Group rows by one or more columns for aggregation ##### keys +A function that receives table references and returns the + field(s) to group by. A query with no aggregate wraps it in `groupKeys()`; + aggregates such as `count()` bring the grouping code themselves. + [`GroupByCallback`](../type-aliases/GroupByCallback.md)\<`TContext`\> | [`GroupKeys`](../interfaces/GroupKeys.md)\<`TContext`\> #### Returns @@ -365,6 +369,12 @@ query category: sales.category, totalSales: sum(sales.amount) })) + +// Group without an aggregate +query + .from({ orders: ordersCollection }) + .groupBy(groupKeys(({orders}) => orders.status)) + .select(({orders}) => ({ status: orders.status })) ``` *** @@ -375,7 +385,7 @@ query having(callback): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:477](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L477) +Defined in: [packages/db/src/query/builder/index.ts:478](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L478) Filter grouped rows based on aggregate conditions @@ -424,7 +434,7 @@ query join(operator): QueryBuilder, TJoinType>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:340](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L340) +Defined in: [packages/db/src/query/builder/index.ts:342](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L342) Join another table or subquery to the current query @@ -444,6 +454,12 @@ Join another table or subquery to the current query [`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `TJoinType`\> +A join operator: `leftJoin`, `innerJoin`, `rightJoin`, + `fullJoin`, or `joinOf(type, ...)`. Each takes an object with a single + key-value pair (the alias and a Collection or subquery) and a callback + that returns the join condition. Pass it inline so the callback is typed + from this query. + #### Returns [`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`MergeContextWithJoinType`](../type-aliases/MergeContextWithJoinType.md)\<`TContext`, [`SchemaFromSource`](../type-aliases/SchemaFromSource.md)\<`TSource`\>, `TJoinType`\>\> @@ -477,7 +493,7 @@ query limit(count): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:734](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L734) +Defined in: [packages/db/src/query/builder/index.ts:743](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L743) Limit the number of rows returned by the query `orderBy` is required for `limit` @@ -514,7 +530,7 @@ query offset(count): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:758](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L758) +Defined in: [packages/db/src/query/builder/index.ts:767](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L767) Skip a number of rows before returning results `orderBy` is required for `offset` @@ -552,7 +568,7 @@ query orderBy(callback, options): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:609](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L609) +Defined in: [packages/db/src/query/builder/index.ts:610](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L610) Sort the query results by one or more columns @@ -604,7 +620,7 @@ query select(callback): QueryBuilder>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:543](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L543) +Defined in: [packages/db/src/query/builder/index.ts:544](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L544) Select specific columns or computed values from the query @@ -663,7 +679,7 @@ query select(callback): QueryBuilder>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:548](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L548) +Defined in: [packages/db/src/query/builder/index.ts:549](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L549) Select specific columns or computed values from the query @@ -806,7 +822,7 @@ query where(callback): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:419](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L419) +Defined in: [packages/db/src/query/builder/index.ts:420](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L420) Filter rows based on a condition diff --git a/docs/reference/functions/add.md b/docs/reference/functions/add.md index 7b970fb86f..2bb2314897 100644 --- a/docs/reference/functions/add.md +++ b/docs/reference/functions/add.md @@ -7,7 +7,7 @@ title: add function add(left, right): BinaryNumericReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:618](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L618) +Defined in: [packages/db/src/query/builder/functions.ts:615](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L615) ## Type Parameters diff --git a/docs/reference/functions/and.md b/docs/reference/functions/and.md index f76163a8dd..74e1f61574 100644 --- a/docs/reference/functions/and.md +++ b/docs/reference/functions/and.md @@ -9,7 +9,7 @@ title: and function and(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:210](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L210) +Defined in: [packages/db/src/query/builder/functions.ts:207](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L207) ### Parameters @@ -34,7 +34,7 @@ function and( rest): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:214](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L214) +Defined in: [packages/db/src/query/builder/functions.ts:211](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L211) ### Parameters diff --git a/docs/reference/functions/avg.md b/docs/reference/functions/avg.md index 0063449b87..c3b5da604e 100644 --- a/docs/reference/functions/avg.md +++ b/docs/reference/functions/avg.md @@ -9,7 +9,7 @@ title: avg function avg(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:664](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L664) +Defined in: [packages/db/src/query/builder/functions.ts:661](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L661) ### Type Parameters @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:664](https://github.com/ function avg(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:665](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L665) +Defined in: [packages/db/src/query/builder/functions.ts:662](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L662) ### Type Parameters @@ -57,7 +57,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:665](https://github.com/ function avg(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:668](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L668) +Defined in: [packages/db/src/query/builder/functions.ts:665](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L665) ### Type Parameters diff --git a/docs/reference/functions/caseWhen.md b/docs/reference/functions/caseWhen.md index 1d3d5b7ac7..d7e6227612 100644 --- a/docs/reference/functions/caseWhen.md +++ b/docs/reference/functions/caseWhen.md @@ -9,7 +9,7 @@ title: caseWhen function caseWhen(condition1, value1): CaseWhenResult<[V1], false>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:408](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L408) +Defined in: [packages/db/src/query/builder/functions.ts:405](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L405) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -83,7 +83,7 @@ function caseWhen( defaultValue): CaseWhenResult<[V1, D], true>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:412](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L412) +Defined in: [packages/db/src/query/builder/functions.ts:409](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L409) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -166,7 +166,7 @@ function caseWhen( value2): CaseWhenResult<[V1, V2], false>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:417](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L417) +Defined in: [packages/db/src/query/builder/functions.ts:414](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L414) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -258,7 +258,7 @@ function caseWhen( defaultValue): CaseWhenResult<[V1, V2, D], true>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:428](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L428) +Defined in: [packages/db/src/query/builder/functions.ts:425](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L425) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -359,7 +359,7 @@ function caseWhen( value3): CaseWhenResult<[V1, V2, V3], false>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:441](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L441) +Defined in: [packages/db/src/query/builder/functions.ts:438](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L438) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -469,7 +469,7 @@ function caseWhen( defaultValue): CaseWhenResult<[V1, V2, V3, D], true>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:456](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L456) +Defined in: [packages/db/src/query/builder/functions.ts:453](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L453) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -588,7 +588,7 @@ function caseWhen( value4): CaseWhenResult<[V1, V2, V3, V4], false>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:473](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L473) +Defined in: [packages/db/src/query/builder/functions.ts:470](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L470) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -716,7 +716,7 @@ function caseWhen( defaultValue): CaseWhenResult<[V1, V2, V3, V4, D], true>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:492](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L492) +Defined in: [packages/db/src/query/builder/functions.ts:489](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L489) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -853,7 +853,7 @@ function caseWhen( value5): CaseWhenResult<[V1, V2, V3, V4, V5], false>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:513](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L513) +Defined in: [packages/db/src/query/builder/functions.ts:510](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L510) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -999,7 +999,7 @@ function caseWhen( defaultValue): CaseWhenResult<[V1, V2, V3, V4, V5, D], true>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:536](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L536) +Defined in: [packages/db/src/query/builder/functions.ts:533](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L533) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -1155,7 +1155,7 @@ function caseWhen( rest): any; ``` -Defined in: [packages/db/src/query/builder/functions.ts:561](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L561) +Defined in: [packages/db/src/query/builder/functions.ts:558](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L558) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. diff --git a/docs/reference/functions/coalesce.md b/docs/reference/functions/coalesce.md index 5b7ba034df..88aa7011fe 100644 --- a/docs/reference/functions/coalesce.md +++ b/docs/reference/functions/coalesce.md @@ -7,7 +7,7 @@ title: coalesce function coalesce(...args): CoalesceReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:358](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L358) +Defined in: [packages/db/src/query/builder/functions.ts:355](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L355) ## Type Parameters diff --git a/docs/reference/functions/compileExpression.md b/docs/reference/functions/compileExpression.md index 86f62d4310..fb7e934830 100644 --- a/docs/reference/functions/compileExpression.md +++ b/docs/reference/functions/compileExpression.md @@ -4,7 +4,7 @@ title: compileExpression --- ```ts -function compileExpression(expr, isSingleRow): CompiledExpression | CompiledSingleRowExpression; +function compileExpression(expr, isSingleRow): CompiledSingleRowExpression | CompiledExpression; ``` Defined in: [packages/db/src/query/compiler/evaluators.ts:103](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/evaluators.ts#L103) @@ -24,4 +24,4 @@ This eliminates branching during evaluation by pre-compiling the expression stru ## Returns -`CompiledExpression` \| `CompiledSingleRowExpression` +`CompiledSingleRowExpression` \| `CompiledExpression` diff --git a/docs/reference/functions/compileQuery.md b/docs/reference/functions/compileQuery.md index 06f9df8351..88263908c3 100644 --- a/docs/reference/functions/compileQuery.md +++ b/docs/reference/functions/compileQuery.md @@ -7,7 +7,7 @@ title: compileQuery function compileQuery(...args): CompilationResult; ``` -Defined in: [packages/db/src/query/compiler/index.ts:409](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/index.ts#L409) +Defined in: [packages/db/src/query/compiler/index.ts:410](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/index.ts#L410) ## Parameters diff --git a/docs/reference/functions/concat.md b/docs/reference/functions/concat.md index 7716a9cd0a..602f877c8d 100644 --- a/docs/reference/functions/concat.md +++ b/docs/reference/functions/concat.md @@ -9,7 +9,7 @@ title: concat function concat(arg): ConcatToArrayWrapper; ``` -Defined in: [packages/db/src/query/builder/functions.ts:308](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L308) +Defined in: [packages/db/src/query/builder/functions.ts:305](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L305) ### Type Parameters @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:308](https://github.com/ function concat(...args): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:311](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L311) +Defined in: [packages/db/src/query/builder/functions.ts:308](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L308) ### Parameters diff --git a/docs/reference/functions/count.md b/docs/reference/functions/count.md index e7d6334203..8f0679f785 100644 --- a/docs/reference/functions/count.md +++ b/docs/reference/functions/count.md @@ -7,7 +7,7 @@ title: count function count(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:660](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L660) +Defined in: [packages/db/src/query/builder/functions.ts:657](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L657) ## Parameters diff --git a/docs/reference/functions/divide.md b/docs/reference/functions/divide.md index 3089398a6b..49911fab34 100644 --- a/docs/reference/functions/divide.md +++ b/docs/reference/functions/divide.md @@ -7,7 +7,7 @@ title: divide function divide(left, right): DivideReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:648](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L648) +Defined in: [packages/db/src/query/builder/functions.ts:645](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L645) ## Type Parameters diff --git a/docs/reference/functions/eq.md b/docs/reference/functions/eq.md index 2c9aec380a..bf6cb6a09b 100644 --- a/docs/reference/functions/eq.md +++ b/docs/reference/functions/eq.md @@ -9,7 +9,7 @@ title: eq function eq(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:144](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L144) +Defined in: [packages/db/src/query/builder/functions.ts:141](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L141) ### Type Parameters @@ -37,7 +37,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:144](https://github.com/ function eq(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:148](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L148) +Defined in: [packages/db/src/query/builder/functions.ts:145](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L145) ### Type Parameters @@ -65,7 +65,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:148](https://github.com/ function eq(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:152](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L152) +Defined in: [packages/db/src/query/builder/functions.ts:149](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L149) ### Type Parameters diff --git a/docs/reference/functions/getLiveQueryHash.md b/docs/reference/functions/getLiveQueryHash.md index 0584039ee9..2bce492710 100644 --- a/docs/reference/functions/getLiveQueryHash.md +++ b/docs/reference/functions/getLiveQueryHash.md @@ -7,7 +7,7 @@ title: getLiveQueryHash function getLiveQueryHash(preparedValue, queryKey?): string; ``` -Defined in: [packages/db/src/live-query-options.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L193) +Defined in: [packages/db/src/live-query-options.ts:201](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L201) ## Parameters diff --git a/docs/reference/functions/getPreparedLiveQueryIdentity.md b/docs/reference/functions/getPreparedLiveQueryIdentity.md index 21a2960981..6597822486 100644 --- a/docs/reference/functions/getPreparedLiveQueryIdentity.md +++ b/docs/reference/functions/getPreparedLiveQueryIdentity.md @@ -7,7 +7,7 @@ title: getPreparedLiveQueryIdentity function getPreparedLiveQueryIdentity(value, pool?): unknown; ``` -Defined in: [packages/db/src/live-query-options.ts:162](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L162) +Defined in: [packages/db/src/live-query-options.ts:167](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L167) ## Parameters diff --git a/docs/reference/functions/getPreparedLiveQuerySources.md b/docs/reference/functions/getPreparedLiveQuerySources.md index 74890ee9f9..befa875738 100644 --- a/docs/reference/functions/getPreparedLiveQuerySources.md +++ b/docs/reference/functions/getPreparedLiveQuerySources.md @@ -7,7 +7,7 @@ title: getPreparedLiveQuerySources function getPreparedLiveQuerySources(value, onUnboundDescriptor?): Collection, string | number, UtilsRecord, StandardSchemaV1, Record>[]; ``` -Defined in: [packages/db/src/live-query-options.ts:139](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L139) +Defined in: [packages/db/src/live-query-options.ts:144](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L144) Concrete source objects in query-position order for a prepared value. diff --git a/docs/reference/functions/gt.md b/docs/reference/functions/gt.md index e897c23f02..a49bb3bb73 100644 --- a/docs/reference/functions/gt.md +++ b/docs/reference/functions/gt.md @@ -9,7 +9,7 @@ title: gt function gt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L157) +Defined in: [packages/db/src/query/builder/functions.ts:154](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L154) ### Type Parameters @@ -37,7 +37,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:157](https://github.com/ function gt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:161](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L161) +Defined in: [packages/db/src/query/builder/functions.ts:158](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L158) ### Type Parameters @@ -65,7 +65,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:161](https://github.com/ function gt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:165](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L165) +Defined in: [packages/db/src/query/builder/functions.ts:162](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L162) ### Type Parameters diff --git a/docs/reference/functions/gte.md b/docs/reference/functions/gte.md index 9c62f339dd..56d40d5158 100644 --- a/docs/reference/functions/gte.md +++ b/docs/reference/functions/gte.md @@ -9,7 +9,7 @@ title: gte function gte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:170](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L170) +Defined in: [packages/db/src/query/builder/functions.ts:167](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L167) ### Type Parameters @@ -37,7 +37,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:170](https://github.com/ function gte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:174](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L174) +Defined in: [packages/db/src/query/builder/functions.ts:171](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L171) ### Type Parameters @@ -65,7 +65,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:174](https://github.com/ function gte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:178](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L178) +Defined in: [packages/db/src/query/builder/functions.ts:175](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L175) ### Type Parameters diff --git a/docs/reference/functions/ilike.md b/docs/reference/functions/ilike.md index a23f1329c3..724e48d7b3 100644 --- a/docs/reference/functions/ilike.md +++ b/docs/reference/functions/ilike.md @@ -7,7 +7,7 @@ title: ilike function ilike(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:281](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L281) +Defined in: [packages/db/src/query/builder/functions.ts:278](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L278) ## Parameters diff --git a/docs/reference/functions/inArray.md b/docs/reference/functions/inArray.md index 1c43b1bffe..c7d1f27535 100644 --- a/docs/reference/functions/inArray.md +++ b/docs/reference/functions/inArray.md @@ -7,7 +7,7 @@ title: inArray function inArray(value, array): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:266](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L266) +Defined in: [packages/db/src/query/builder/functions.ts:263](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L263) ## Parameters diff --git a/docs/reference/functions/isNull.md b/docs/reference/functions/isNull.md index 55127b6509..bf463bf3e1 100644 --- a/docs/reference/functions/isNull.md +++ b/docs/reference/functions/isNull.md @@ -7,7 +7,7 @@ title: isNull function isNull(value): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:262](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L262) +Defined in: [packages/db/src/query/builder/functions.ts:259](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L259) ## Parameters diff --git a/docs/reference/functions/isUndefined.md b/docs/reference/functions/isUndefined.md index 71b4aa88da..8373acbad6 100644 --- a/docs/reference/functions/isUndefined.md +++ b/docs/reference/functions/isUndefined.md @@ -7,7 +7,7 @@ title: isUndefined function isUndefined(value): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:258](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L258) +Defined in: [packages/db/src/query/builder/functions.ts:255](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L255) ## Parameters diff --git a/docs/reference/functions/joinOf.md b/docs/reference/functions/joinOf.md index 129a4d2153..011778a2b2 100644 --- a/docs/reference/functions/joinOf.md +++ b/docs/reference/functions/joinOf.md @@ -10,7 +10,7 @@ function joinOf( on): JoinOperator; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L58) +Defined in: [packages/db/src/query/builder/join-operators.ts:63](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L63) A join whose type is chosen at run time. diff --git a/docs/reference/functions/length.md b/docs/reference/functions/length.md index 71c924bb5f..91e11a5e03 100644 --- a/docs/reference/functions/length.md +++ b/docs/reference/functions/length.md @@ -7,7 +7,7 @@ title: length function length(arg): NumericFunctionReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:302](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L302) +Defined in: [packages/db/src/query/builder/functions.ts:299](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L299) ## Type Parameters diff --git a/docs/reference/functions/like.md b/docs/reference/functions/like.md index 94ec7396e1..9cb1feb472 100644 --- a/docs/reference/functions/like.md +++ b/docs/reference/functions/like.md @@ -7,7 +7,7 @@ title: like function like(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:273](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L273) +Defined in: [packages/db/src/query/builder/functions.ts:270](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L270) ## Parameters diff --git a/docs/reference/functions/lower.md b/docs/reference/functions/lower.md index f2e1653fb6..d7cd7d45f4 100644 --- a/docs/reference/functions/lower.md +++ b/docs/reference/functions/lower.md @@ -7,7 +7,7 @@ title: lower function lower(arg): StringFunctionReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:296](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L296) +Defined in: [packages/db/src/query/builder/functions.ts:293](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L293) ## Type Parameters diff --git a/docs/reference/functions/lt.md b/docs/reference/functions/lt.md index 682ec84fad..5f7ccdcb35 100644 --- a/docs/reference/functions/lt.md +++ b/docs/reference/functions/lt.md @@ -9,7 +9,7 @@ title: lt function lt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:183](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L183) +Defined in: [packages/db/src/query/builder/functions.ts:180](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L180) ### Type Parameters @@ -37,7 +37,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:183](https://github.com/ function lt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:187](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L187) +Defined in: [packages/db/src/query/builder/functions.ts:184](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L184) ### Type Parameters @@ -65,7 +65,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:187](https://github.com/ function lt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:191](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L191) +Defined in: [packages/db/src/query/builder/functions.ts:188](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L188) ### Type Parameters diff --git a/docs/reference/functions/lte.md b/docs/reference/functions/lte.md index 77065b21c4..642306d6a3 100644 --- a/docs/reference/functions/lte.md +++ b/docs/reference/functions/lte.md @@ -9,7 +9,7 @@ title: lte function lte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:196](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L196) +Defined in: [packages/db/src/query/builder/functions.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L193) ### Type Parameters @@ -37,7 +37,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:196](https://github.com/ function lte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:200](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L200) +Defined in: [packages/db/src/query/builder/functions.ts:197](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L197) ### Type Parameters @@ -65,7 +65,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:200](https://github.com/ function lte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:204](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L204) +Defined in: [packages/db/src/query/builder/functions.ts:201](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L201) ### Type Parameters diff --git a/docs/reference/functions/max.md b/docs/reference/functions/max.md index 2f2ee4578b..03ac6b93fd 100644 --- a/docs/reference/functions/max.md +++ b/docs/reference/functions/max.md @@ -9,7 +9,7 @@ title: max function max(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:695](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L695) +Defined in: [packages/db/src/query/builder/functions.ts:692](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L692) ### Type Parameters @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:695](https://github.com/ function max(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:696](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L696) +Defined in: [packages/db/src/query/builder/functions.ts:693](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L693) ### Type Parameters @@ -57,7 +57,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:696](https://github.com/ function max(arg): Aggregate>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:697](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L697) +Defined in: [packages/db/src/query/builder/functions.ts:694](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L694) ### Type Parameters diff --git a/docs/reference/functions/min.md b/docs/reference/functions/min.md index f707c512fe..af5d19b1b8 100644 --- a/docs/reference/functions/min.md +++ b/docs/reference/functions/min.md @@ -9,7 +9,7 @@ title: min function min(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:686](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L686) +Defined in: [packages/db/src/query/builder/functions.ts:683](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L683) ### Type Parameters @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:686](https://github.com/ function min(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:687](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L687) +Defined in: [packages/db/src/query/builder/functions.ts:684](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L684) ### Type Parameters @@ -57,7 +57,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:687](https://github.com/ function min(arg): Aggregate>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:688](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L688) +Defined in: [packages/db/src/query/builder/functions.ts:685](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L685) ### Type Parameters diff --git a/docs/reference/functions/multiply.md b/docs/reference/functions/multiply.md index ac9cf6c98b..be91130f70 100644 --- a/docs/reference/functions/multiply.md +++ b/docs/reference/functions/multiply.md @@ -7,7 +7,7 @@ title: multiply function multiply(left, right): BinaryNumericReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:638](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L638) +Defined in: [packages/db/src/query/builder/functions.ts:635](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L635) ## Type Parameters diff --git a/docs/reference/functions/not.md b/docs/reference/functions/not.md index d1a7683025..1100b8f958 100644 --- a/docs/reference/functions/not.md +++ b/docs/reference/functions/not.md @@ -7,7 +7,7 @@ title: not function not(value): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:253](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L253) +Defined in: [packages/db/src/query/builder/functions.ts:250](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L250) ## Parameters diff --git a/docs/reference/functions/or.md b/docs/reference/functions/or.md index fa499aa89f..65206480c0 100644 --- a/docs/reference/functions/or.md +++ b/docs/reference/functions/or.md @@ -9,7 +9,7 @@ title: or function or(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:232](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L232) +Defined in: [packages/db/src/query/builder/functions.ts:229](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L229) ### Parameters @@ -34,7 +34,7 @@ function or( rest): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:236](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L236) +Defined in: [packages/db/src/query/builder/functions.ts:233](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L233) ### Parameters diff --git a/docs/reference/functions/prepareLiveQueryValue.md b/docs/reference/functions/prepareLiveQueryValue.md index 9fc6718bf1..6590d5d4da 100644 --- a/docs/reference/functions/prepareLiveQueryValue.md +++ b/docs/reference/functions/prepareLiveQueryValue.md @@ -10,7 +10,7 @@ function prepareLiveQueryValue( deferredCollections?): unknown; ``` -Defined in: [packages/db/src/live-query-options.ts:86](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L86) +Defined in: [packages/db/src/live-query-options.ts:87](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L87) ## Parameters diff --git a/docs/reference/functions/resolveLiveQueryValue.md b/docs/reference/functions/resolveLiveQueryValue.md index 1f4f3d0b50..62d62fbfe5 100644 --- a/docs/reference/functions/resolveLiveQueryValue.md +++ b/docs/reference/functions/resolveLiveQueryValue.md @@ -9,7 +9,7 @@ function resolveLiveQueryValue(value, __namedParameters): | null; ``` -Defined in: [packages/db/src/live-query-options.ts:232](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L232) +Defined in: [packages/db/src/live-query-options.ts:240](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L240) ## Parameters diff --git a/docs/reference/functions/resumeDeferredLiveQueryCollections.md b/docs/reference/functions/resumeDeferredLiveQueryCollections.md index 6a945d103d..4d99cb742e 100644 --- a/docs/reference/functions/resumeDeferredLiveQueryCollections.md +++ b/docs/reference/functions/resumeDeferredLiveQueryCollections.md @@ -7,7 +7,7 @@ title: resumeDeferredLiveQueryCollections function resumeDeferredLiveQueryCollections(collections): void; ``` -Defined in: [packages/db/src/live-query-options.ts:33](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L33) +Defined in: [packages/db/src/live-query-options.ts:34](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L34) Release every deferred source, then report the first startup failure. diff --git a/docs/reference/functions/subtract.md b/docs/reference/functions/subtract.md index 166f88eacf..10a86945f9 100644 --- a/docs/reference/functions/subtract.md +++ b/docs/reference/functions/subtract.md @@ -7,7 +7,7 @@ title: subtract function subtract(left, right): BinaryNumericReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:628](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L628) +Defined in: [packages/db/src/query/builder/functions.ts:625](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L625) ## Type Parameters diff --git a/docs/reference/functions/sum.md b/docs/reference/functions/sum.md index 3ae3595f8f..fb700a051f 100644 --- a/docs/reference/functions/sum.md +++ b/docs/reference/functions/sum.md @@ -9,7 +9,7 @@ title: sum function sum(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:675](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L675) +Defined in: [packages/db/src/query/builder/functions.ts:672](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L672) ### Type Parameters @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:675](https://github.com/ function sum(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:676](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L676) +Defined in: [packages/db/src/query/builder/functions.ts:673](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L673) ### Type Parameters @@ -57,7 +57,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:676](https://github.com/ function sum(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:679](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L679) +Defined in: [packages/db/src/query/builder/functions.ts:676](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L676) ### Type Parameters diff --git a/docs/reference/functions/upper.md b/docs/reference/functions/upper.md index cd06a71664..5064c1f6dc 100644 --- a/docs/reference/functions/upper.md +++ b/docs/reference/functions/upper.md @@ -7,7 +7,7 @@ title: upper function upper(arg): StringFunctionReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:290](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L290) +Defined in: [packages/db/src/query/builder/functions.ts:287](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L287) ## Type Parameters diff --git a/docs/reference/index.md b/docs/reference/index.md index 23f79b3596..6fd50ea7e1 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -260,6 +260,7 @@ title: "@tanstack/db" - [KeyedNamespacedRow](type-aliases/KeyedNamespacedRow.md) - [KeyedStream](type-aliases/KeyedStream.md) - [LiveQueryCollectionUtils](type-aliases/LiveQueryCollectionUtils.md) +- [LiveQueryHookConfig](type-aliases/LiveQueryHookConfig.md) - [LiveQueryKey](type-aliases/LiveQueryKey.md) - [LiveQueryObserverListener](type-aliases/LiveQueryObserverListener.md) - [LiveQueryOptions](type-aliases/LiveQueryOptions.md) diff --git a/docs/reference/interfaces/JoinOperator.md b/docs/reference/interfaces/JoinOperator.md index 11eac38a47..9dac574ed2 100644 --- a/docs/reference/interfaces/JoinOperator.md +++ b/docs/reference/interfaces/JoinOperator.md @@ -3,7 +3,7 @@ id: JoinOperator title: JoinOperator --- -Defined in: [packages/db/src/query/builder/join-operators.ts:19](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L19) +Defined in: [packages/db/src/query/builder/join-operators.ts:17](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L17) A join step for `query.join(...)`. Importing a join operator is what brings the join compiler into a bundle. @@ -24,192 +24,214 @@ the join compiler into a bundle. ## Properties -### compile() +### on + +```ts +on: JoinOnCallback ? InferCollectionType : TSource[K] extends CollectionOptionsIdentity ? InferCollectionType : TSource[K] extends QueryBuilder ? ResultValue : never }[K] }>>; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:23](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L23) + +*** + +### runtime ```ts -compile: (pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, localFrom, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, valueIdentity, parentKeyStream?) => NamespacedAndKeyedStream; +runtime: object; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:29](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L29) +Defined in: [packages/db/src/query/builder/join-operators.ts:27](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L27) + +#### compile() + +```ts +compile: (pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, localFrom, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, valueIdentity, parentKeyStream?) => NamespacedAndKeyedStream = processJoins; +``` Processes all join clauses, applying lazy loading optimizations and maintaining alias tracking for per-alias subscriptions (enables self-joins). -#### Parameters +##### Parameters -##### pipeline +###### pipeline [`NamespacedAndKeyedStream`](../type-aliases/NamespacedAndKeyedStream.md) -##### joinClauses +###### joinClauses [`JoinClause`](../@tanstack/namespaces/IR/interfaces/JoinClause.md)[] -##### sources +###### sources `Record`\<`string`, [`KeyedStream`](../type-aliases/KeyedStream.md)\> -##### mainCollectionId +###### mainCollectionId `string` -##### mainSource +###### mainSource `string` -##### allInputs +###### allInputs `Record`\<`string`, [`KeyedStream`](../type-aliases/KeyedStream.md)\> -##### cache +###### cache `QueryCache` -##### queryMapping +###### queryMapping `QueryMapping` -##### collections +###### collections `Record`\<`string`, [`Collection`](Collection.md)\> -##### subscriptions +###### subscriptions `Record`\<`string`, `CollectionSubscription`\> -##### callbacks +###### callbacks `Record`\<`string`, `LazyCollectionCallbacks`\> -##### lazySources +###### lazySources `Set`\<`string`\> -##### optimizableOrderByCollections +###### optimizableOrderByCollections `Record`\<`string`, `OrderByOptimizationInfo`\> -##### setWindowFn +###### setWindowFn (`windowFn`) => `void` -##### rawQuery +###### rawQuery [`QueryIR`](../@tanstack/namespaces/IR/interfaces/QueryIR.md) -##### localFrom +###### localFrom [`From`](../@tanstack/namespaces/IR/type-aliases/From.md) -##### onCompileSubquery +###### onCompileSubquery (...`args`) => `CompilationResult` -##### aliasToCollectionId +###### aliasToCollectionId `Record`\<`string`, `string`\> -##### aliasRemapping +###### aliasRemapping `Record`\<`string`, `string`\> -##### sourceWhereClauses +###### sourceWhereClauses `Map`\<`string`, [`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md)\<`boolean`\>\> -##### mainSourceIsParentFiltered +###### mainSourceIsParentFiltered `boolean` -##### valueIdentity +###### valueIdentity `ValueIdentity` -##### parentKeyStream? +###### parentKeyStream? [`KeyedStream`](../type-aliases/KeyedStream.md) -#### Returns +##### Returns [`NamespacedAndKeyedStream`](../type-aliases/NamespacedAndKeyedStream.md) -*** - -### materialize() +#### optimize() ```ts -materialize: (compilation, getRootKey?, reduceJoinedPublicKeys) => MaterializedRootCompilation; +optimize: (query, groupedClauses) => QueryIR = applyOptimizations; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:31](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L31) +Step 4: Apply optimizations by lifting single-source clauses into subqueries. -Compiles inline includes into the same D2 graph as their parent relation. -Collection-valued includes become inert bucket references. The public facade -adapter resolves those references after the graph reaches quiescence. +Creates a new QueryIR with single-source WHERE clauses moved to subqueries +that wrap the original table references. This ensures immutability and prevents +infinite recursion issues. -#### Parameters +##### Parameters -##### compilation +###### query -`CompilationResult` +[`QueryIR`](../@tanstack/namespaces/IR/interfaces/QueryIR.md) -##### getRootKey? +Original QueryIR to optimize -(`row`) => `unknown` +###### groupedClauses -##### reduceJoinedPublicKeys? +`GroupedWhereClauses` -`boolean` = `false` +WHERE clauses grouped by optimization strategy -#### Returns +##### Returns -`MaterializedRootCompilation` +[`QueryIR`](../@tanstack/namespaces/IR/interfaces/QueryIR.md) -*** +New QueryIR with optimizations applied -### on +#### reducePublicKeys() ```ts -on: JoinOnCallback ? InferCollectionType : TSource[K] extends CollectionOptionsIdentity ? InferCollectionType : TSource[K] extends QueryBuilder ? ResultValue : never }[K] }>>; +reducePublicKeys: (compilation, getKey) => object; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:25](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L25) +Reduces a query without includes to one canonical row per public key. -*** +##### Parameters -### optimize() +###### compilation -```ts -optimize: (query, groupedClauses) => QueryIR; -``` +`CompilationResult` -Defined in: [packages/db/src/query/builder/join-operators.ts:30](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L30) +###### getKey -Step 4: Apply optimizations by lifting single-source clauses into subqueries. +(`row`) => `unknown` -Creates a new QueryIR with single-source WHERE clauses moved to subqueries -that wrap the original table references. This ensures immutability and prevents -infinite recursion issues. +##### Returns -#### Parameters +`object` -##### query +###### facades -[`QueryIR`](../@tanstack/namespaces/IR/interfaces/QueryIR.md) +```ts +facades: undefined; +``` -Original QueryIR to optimize +###### pipeline -##### groupedClauses +```ts +pipeline: ResultStream; +``` -`GroupedWhereClauses` +#### validate() -WHERE clauses grouped by optimization strategy +```ts +validate: (expression) => readonly [BasicExpression, BasicExpression][] = validateJoinConditions; +``` -#### Returns +Validate join syntax and return its equality operands in source order. -[`QueryIR`](../@tanstack/namespaces/IR/interfaces/QueryIR.md) +##### Parameters -New QueryIR with optimizations applied +###### expression + +[`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md) + +##### Returns + +readonly \[[`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md), [`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md)\][] *** @@ -219,7 +241,7 @@ New QueryIR with optimizations applied source: TSource; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L24) +Defined in: [packages/db/src/query/builder/join-operators.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L22) *** @@ -229,26 +251,4 @@ Defined in: [packages/db/src/query/builder/join-operators.ts:24](https://github. type: TJoinType; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L28) - -*** - -### validate() - -```ts -validate: (expression) => readonly [BasicExpression, BasicExpression][]; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:32](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L32) - -Validate join syntax and return its equality operands in source order. - -#### Parameters - -##### expression - -[`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md) - -#### Returns - -readonly \[[`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md), [`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md)\][] +Defined in: [packages/db/src/query/builder/join-operators.ts:26](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L26) diff --git a/docs/reference/interfaces/LiveQueryCollectionConfig.md b/docs/reference/interfaces/LiveQueryCollectionConfig.md index 2b27dd4e56..d2574e07a6 100644 --- a/docs/reference/interfaces/LiveQueryCollectionConfig.md +++ b/docs/reference/interfaces/LiveQueryCollectionConfig.md @@ -3,31 +3,7 @@ id: LiveQueryCollectionConfig title: LiveQueryCollectionConfig --- -Defined in: [packages/db/src/query/live/types.ts:66](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L66) - -Configuration interface for live query collection options - -## Example - -```typescript -const config: LiveQueryCollectionConfig = { - // id is optional - will auto-generate "live-query-1", "live-query-2", etc. - query: (q) => q - .from({ comment: commentsCollection }) - .join( - { user: usersCollection }, - ({ comment, user }) => eq(comment.user_id, user.id) - ) - .where(({ comment }) => eq(comment.active, true)) - .select(({ comment, user }) => ({ - id: comment.id, - content: comment.content, - authorName: user.name, - })), - // getKey is optional - defaults to using stream key - getKey: (item) => item.id, -} -``` +Defined in: [packages/db/src/query/live/types.ts:79](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L79) ## Type Parameters @@ -47,7 +23,7 @@ const config: LiveQueryCollectionConfig = { optional defaultStringCollation: StringCollationConfig; ``` -Defined in: [packages/db/src/query/live/types.ts:130](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L130) +Defined in: [packages/db/src/query/live/types.ts:136](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L136) Optional compare options for string sorting. If provided, these will be used instead of inheriting from the FROM collection. @@ -60,7 +36,7 @@ If provided, these will be used instead of inheriting from the FROM collection. optional gcTime: number; ``` -Defined in: [packages/db/src/query/live/types.ts:119](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L119) +Defined in: [packages/db/src/query/live/types.ts:125](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L125) GC time for the collection @@ -72,7 +48,7 @@ GC time for the collection optional getKey: (item) => string | number; ``` -Defined in: [packages/db/src/query/live/types.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L97) +Defined in: [packages/db/src/query/live/types.ts:103](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L103) Function to extract the key from result items If not provided, defaults to using the key from the D2 stream @@ -95,7 +71,7 @@ If not provided, defaults to using the key from the D2 stream optional id: string; ``` -Defined in: [packages/db/src/query/live/types.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L74) +Defined in: [packages/db/src/query/live/types.ts:87](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L87) Unique identifier for the collection If not provided, defaults to `live-query-${number}` with auto-incrementing number @@ -110,7 +86,7 @@ optional onDelete: | DeleteMutationFn; ``` -Defined in: [packages/db/src/query/live/types.ts:109](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L109) +Defined in: [packages/db/src/query/live/types.ts:115](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L115) *** @@ -122,7 +98,7 @@ optional onInsert: | InsertMutationFn; ``` -Defined in: [packages/db/src/query/live/types.ts:107](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L107) +Defined in: [packages/db/src/query/live/types.ts:113](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L113) Optional mutation handlers @@ -136,70 +112,7 @@ optional onUpdate: | UpdateMutationFn; ``` -Defined in: [packages/db/src/query/live/types.ts:108](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L108) - -*** - -### pool? - -```ts -optional pool: object; -``` - -Defined in: [packages/db/src/query/live/types.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L82) - -Framework hooks only: pass `pooledLiveQueries` to serve this query from a -shared equality partition when its shape allows. A config with any option -other than `query`, `gcTime`, and `pool` compiles its own Collection. -`createLiveQueryCollection` does not pool. - -#### createPooledLiveQuery() - -```ts -createPooledLiveQuery: (query, __namedParameters) => - | Collection, any> - | undefined; -``` - -A pooled view for a query a partition can serve, or undefined. The view is -typed as the Collection it stands in for. - -##### Parameters - -###### query - -[`BaseQueryBuilder`](../classes/BaseQueryBuilder.md) - -###### \_\_namedParameters - -###### gcTime? - -`number` = `5_000` - -##### Returns - - \| [`Collection`](Collection.md)\<`any`, `any`, `any`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `any`\> - \| `undefined` - -#### getPooledQueryIdentity() - -```ts -getPooledQueryIdentity: (query) => string | undefined; -``` - -The identity of a query a partition can serve with no residual conjunct: -its source and its `eq` fields and literals, which determine its rows. -Undefined for any other query, which keeps the full structural identity. - -##### Parameters - -###### query - -[`BaseQueryBuilder`](../classes/BaseQueryBuilder.md) - -##### Returns - -`string` \| `undefined` +Defined in: [packages/db/src/query/live/types.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L114) *** @@ -211,7 +124,7 @@ query: | QueryBuilder & RootObjectResultConstraint; ``` -Defined in: [packages/db/src/query/live/types.ts:87](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L87) +Defined in: [packages/db/src/query/live/types.ts:93](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L93) Query builder function that defines the live query @@ -223,7 +136,7 @@ Query builder function that defines the live query optional schema: undefined; ``` -Defined in: [packages/db/src/query/live/types.ts:102](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L102) +Defined in: [packages/db/src/query/live/types.ts:108](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L108) Optional schema for validation @@ -235,7 +148,7 @@ Optional schema for validation optional singleResult: true; ``` -Defined in: [packages/db/src/query/live/types.ts:124](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L124) +Defined in: [packages/db/src/query/live/types.ts:130](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L130) If enabled the collection will return a single object instead of an array @@ -247,6 +160,6 @@ If enabled the collection will return a single object instead of an array optional startSync: boolean; ``` -Defined in: [packages/db/src/query/live/types.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L114) +Defined in: [packages/db/src/query/live/types.ts:120](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L120) Start sync / the query immediately diff --git a/docs/reference/type-aliases/DeferredLiveQueryCollections.md b/docs/reference/type-aliases/DeferredLiveQueryCollections.md index 10ff5b3b98..1dd255e319 100644 --- a/docs/reference/type-aliases/DeferredLiveQueryCollections.md +++ b/docs/reference/type-aliases/DeferredLiveQueryCollections.md @@ -7,4 +7,4 @@ title: DeferredLiveQueryCollections type DeferredLiveQueryCollections = Set>; ``` -Defined in: [packages/db/src/live-query-options.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L28) +Defined in: [packages/db/src/live-query-options.ts:29](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L29) diff --git a/docs/reference/type-aliases/ExtractContext.md b/docs/reference/type-aliases/ExtractContext.md index 1a96c351e8..c3fb4fcb15 100644 --- a/docs/reference/type-aliases/ExtractContext.md +++ b/docs/reference/type-aliases/ExtractContext.md @@ -7,7 +7,7 @@ title: ExtractContext type ExtractContext = T extends BaseQueryBuilder ? TContext : T extends QueryBuilder ? TContext : never; ``` -Defined in: [packages/db/src/query/builder/index.ts:1765](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1765) +Defined in: [packages/db/src/query/builder/index.ts:1788](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1788) ## Type Parameters diff --git a/docs/reference/type-aliases/InitialQueryBuilder.md b/docs/reference/type-aliases/InitialQueryBuilder.md index a8a43e2c62..d7f4ff9e1c 100644 --- a/docs/reference/type-aliases/InitialQueryBuilder.md +++ b/docs/reference/type-aliases/InitialQueryBuilder.md @@ -7,4 +7,4 @@ title: InitialQueryBuilder type InitialQueryBuilder = Pick, "from" | "unionAll">; ``` -Defined in: [packages/db/src/query/builder/index.ts:1748](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1748) +Defined in: [packages/db/src/query/builder/index.ts:1771](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1771) diff --git a/docs/reference/type-aliases/JoinType.md b/docs/reference/type-aliases/JoinType.md index a7868ad281..cdd748b79c 100644 --- a/docs/reference/type-aliases/JoinType.md +++ b/docs/reference/type-aliases/JoinType.md @@ -7,4 +7,4 @@ title: JoinType type JoinType = "inner" | "left" | "right" | "full"; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:13](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L13) +Defined in: [packages/db/src/query/builder/join-operators.ts:11](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L11) diff --git a/docs/reference/type-aliases/LiveQueryHookConfig.md b/docs/reference/type-aliases/LiveQueryHookConfig.md new file mode 100644 index 0000000000..c986dde019 --- /dev/null +++ b/docs/reference/type-aliases/LiveQueryHookConfig.md @@ -0,0 +1,33 @@ +--- +id: LiveQueryHookConfig +title: LiveQueryHookConfig +--- + +```ts +type LiveQueryHookConfig = LiveQueryCollectionConfig & object; +``` + +Defined in: [packages/db/src/query/live/types.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L72) + +A live query config that a framework hook accepts. Pass `pooledLiveQueries` +as `pool` to serve the query from a shared equality partition when its +shape allows. A config with any option other than `query`, `gcTime`, and +`pool` compiles its own Collection. + +## Type Declaration + +### pool? + +```ts +optional pool: PooledLiveQueries; +``` + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) + +### TResult + +`TResult` *extends* `object` = `RootQueryResult`\<`TContext`\> diff --git a/docs/reference/type-aliases/LiveQueryKey.md b/docs/reference/type-aliases/LiveQueryKey.md index 76869a8605..7c7e49dc8a 100644 --- a/docs/reference/type-aliases/LiveQueryKey.md +++ b/docs/reference/type-aliases/LiveQueryKey.md @@ -7,4 +7,4 @@ title: LiveQueryKey type LiveQueryKey = ReadonlyArray; ``` -Defined in: [packages/db/src/live-query-options.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L22) +Defined in: [packages/db/src/live-query-options.ts:23](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L23) diff --git a/docs/reference/type-aliases/LiveQueryOptions.md b/docs/reference/type-aliases/LiveQueryOptions.md index 3232d637b7..94ad77c9f1 100644 --- a/docs/reference/type-aliases/LiveQueryOptions.md +++ b/docs/reference/type-aliases/LiveQueryOptions.md @@ -7,7 +7,7 @@ title: LiveQueryOptions type LiveQueryOptions = LiveQueryCollectionConfig & object; ``` -Defined in: [packages/db/src/live-query-options.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L24) +Defined in: [packages/db/src/live-query-options.ts:25](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L25) ## Type Declaration diff --git a/docs/reference/type-aliases/OperatorName.md b/docs/reference/type-aliases/OperatorName.md index cfafd17391..7074f85e05 100644 --- a/docs/reference/type-aliases/OperatorName.md +++ b/docs/reference/type-aliases/OperatorName.md @@ -7,4 +7,4 @@ title: OperatorName type OperatorName = typeof operators[number]; ``` -Defined in: [packages/db/src/query/builder/functions.ts:763](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L763) +Defined in: [packages/db/src/query/builder/functions.ts:760](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L760) diff --git a/docs/reference/type-aliases/PooledLiveQueries.md b/docs/reference/type-aliases/PooledLiveQueries.md index 4ddc740a5e..159f98044a 100644 --- a/docs/reference/type-aliases/PooledLiveQueries.md +++ b/docs/reference/type-aliases/PooledLiveQueries.md @@ -7,4 +7,4 @@ title: PooledLiveQueries type PooledLiveQueries = typeof pooledLiveQueries; ``` -Defined in: [packages/db/src/query/pooled-live-query.ts:735](https://github.com/TanStack/db/blob/main/packages/db/src/query/pooled-live-query.ts#L735) +Defined in: [packages/db/src/query/pooled-live-query.ts:738](https://github.com/TanStack/db/blob/main/packages/db/src/query/pooled-live-query.ts#L738) diff --git a/docs/reference/type-aliases/QueryBuilder.md b/docs/reference/type-aliases/QueryBuilder.md index d54cb4c8b4..6fe434406b 100644 --- a/docs/reference/type-aliases/QueryBuilder.md +++ b/docs/reference/type-aliases/QueryBuilder.md @@ -7,7 +7,7 @@ title: QueryBuilder type QueryBuilder = Omit, "from" | "unionAll" | "_getQuery">; ``` -Defined in: [packages/db/src/query/builder/index.ts:1755](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1755) +Defined in: [packages/db/src/query/builder/index.ts:1778](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1778) ## Type Parameters diff --git a/docs/reference/type-aliases/QueryResult.md b/docs/reference/type-aliases/QueryResult.md index 307b7ec106..cc8180623f 100644 --- a/docs/reference/type-aliases/QueryResult.md +++ b/docs/reference/type-aliases/QueryResult.md @@ -7,7 +7,7 @@ title: QueryResult type QueryResult = GetResult>; ``` -Defined in: [packages/db/src/query/builder/index.ts:1773](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1773) +Defined in: [packages/db/src/query/builder/index.ts:1796](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1796) ## Type Parameters diff --git a/docs/reference/variables/Query.md b/docs/reference/variables/Query.md index 06c6923357..dea94b2935 100644 --- a/docs/reference/variables/Query.md +++ b/docs/reference/variables/Query.md @@ -7,4 +7,4 @@ title: Query const Query: InitialQueryBuilderConstructor = BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:1762](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1762) +Defined in: [packages/db/src/query/builder/index.ts:1785](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1785) diff --git a/docs/reference/variables/fullJoin.md b/docs/reference/variables/fullJoin.md index 5a53de6f1b..dea904f2d5 100644 --- a/docs/reference/variables/fullJoin.md +++ b/docs/reference/variables/fullJoin.md @@ -4,31 +4,7 @@ title: fullJoin --- ```ts -const fullJoin: (source, on) => JoinOperator; +const fullJoin: JoinOperatorFunction<"full">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:55](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L55) - -## Type Parameters - -### TContext - -`TContext` *extends* [`Context`](../interfaces/Context.md) - -### TSource - -`TSource` *extends* [`Source`](../type-aliases/Source.md) - -## Parameters - -### source - -`TSource` - -### on - -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> - -## Returns - -[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `"full"`\> +Defined in: [packages/db/src/query/builder/join-operators.ts:60](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L60) diff --git a/docs/reference/variables/innerJoin.md b/docs/reference/variables/innerJoin.md index a6ff95ec1e..07f435f350 100644 --- a/docs/reference/variables/innerJoin.md +++ b/docs/reference/variables/innerJoin.md @@ -4,31 +4,7 @@ title: innerJoin --- ```ts -const innerJoin: (source, on) => JoinOperator; +const innerJoin: JoinOperatorFunction<"inner">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L54) - -## Type Parameters - -### TContext - -`TContext` *extends* [`Context`](../interfaces/Context.md) - -### TSource - -`TSource` *extends* [`Source`](../type-aliases/Source.md) - -## Parameters - -### source - -`TSource` - -### on - -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> - -## Returns - -[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `"inner"`\> +Defined in: [packages/db/src/query/builder/join-operators.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L59) diff --git a/docs/reference/variables/leftJoin.md b/docs/reference/variables/leftJoin.md index 3112accde7..adc7092f16 100644 --- a/docs/reference/variables/leftJoin.md +++ b/docs/reference/variables/leftJoin.md @@ -4,31 +4,7 @@ title: leftJoin --- ```ts -const leftJoin: (source, on) => JoinOperator; +const leftJoin: JoinOperatorFunction<"left">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:52](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L52) - -## Type Parameters - -### TContext - -`TContext` *extends* [`Context`](../interfaces/Context.md) - -### TSource - -`TSource` *extends* [`Source`](../type-aliases/Source.md) - -## Parameters - -### source - -`TSource` - -### on - -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> - -## Returns - -[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `"left"`\> +Defined in: [packages/db/src/query/builder/join-operators.ts:57](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L57) diff --git a/docs/reference/variables/operators.md b/docs/reference/variables/operators.md index a1b51fbdda..8ecb781d35 100644 --- a/docs/reference/variables/operators.md +++ b/docs/reference/variables/operators.md @@ -7,6 +7,6 @@ title: operators const operators: readonly ["eq", "gt", "gte", "lt", "lte", "in", "like", "ilike", "and", "or", "not", "isNull", "isUndefined", "upper", "lower", "length", "concat", "add", "subtract", "multiply", "divide", "coalesce", "caseWhen", "count", "avg", "sum", "min", "max"]; ``` -Defined in: [packages/db/src/query/builder/functions.ts:725](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L725) +Defined in: [packages/db/src/query/builder/functions.ts:722](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L722) All supported operator names in TanStack DB expressions diff --git a/docs/reference/variables/rightJoin.md b/docs/reference/variables/rightJoin.md index d8bb3d1246..c7698c7dde 100644 --- a/docs/reference/variables/rightJoin.md +++ b/docs/reference/variables/rightJoin.md @@ -4,31 +4,7 @@ title: rightJoin --- ```ts -const rightJoin: (source, on) => JoinOperator; +const rightJoin: JoinOperatorFunction<"right">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:53](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L53) - -## Type Parameters - -### TContext - -`TContext` *extends* [`Context`](../interfaces/Context.md) - -### TSource - -`TSource` *extends* [`Source`](../type-aliases/Source.md) - -## Parameters - -### source - -`TSource` - -### on - -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> - -## Returns - -[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `"right"`\> +Defined in: [packages/db/src/query/builder/join-operators.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L58) diff --git a/packages/angular-db/skills/angular-db/SKILL.md b/packages/angular-db/skills/angular-db/SKILL.md index 8464571ca5..b1f466b9e1 100644 --- a/packages/angular-db/skills/angular-db/SKILL.md +++ b/packages/angular-db/skills/angular-db/SKILL.md @@ -188,7 +188,7 @@ Angular 16 structural directives: ## Includes (Hierarchical Data) -When a query uses includes (subqueries in `select`), each child field is a live `Collection` by default. Subscribe to it with `injectLiveQuery` in a child component: +When a query uses `include()` for a subquery in `select`, each child field is a live `Collection`. Subscribe to it with `injectLiveQuery` in a child component: ```typescript @Component({ @@ -209,10 +209,10 @@ export class ProjectListComponent { q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q + issues: include(q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ id: i.id, title: i.title })), + .select(({ i }) => ({ id: i.id, title: i.title }))), })), ) } diff --git a/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts b/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts index 582992e8e7..f40b75ee61 100644 --- a/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts +++ b/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts @@ -1,3 +1,8 @@ +/** + * Joins Test Suite + * + * Tests multi-collection joins with various syncMode combinations + */ import { randomUUID } from 'node:crypto' import { createLiveQueryCollection, @@ -6,11 +11,6 @@ import { isNull, leftJoin, } from '@tanstack/db' -/** - * Joins Test Suite - * - * Tests multi-collection joins with various syncMode combinations - */ import { describe, expect, it, vi } from 'vitest' import { waitFor, waitForQueryData } from '../utils/helpers' diff --git a/packages/db/skills/db-core/live-queries/SKILL.md b/packages/db/skills/db-core/live-queries/SKILL.md index 78b2a9395c..1b71f41900 100644 --- a/packages/db/skills/db-core/live-queries/SKILL.md +++ b/packages/db/skills/db-core/live-queries/SKILL.md @@ -115,17 +115,16 @@ non-null value. ### 2. Joining two collections -Join conditions accept `eq()` or a nonempty, possibly nested `and()` of equalities (equality only -- IVM constraint). Default join type is `left`. Convenience methods: `leftJoin`, `rightJoin`, `innerJoin`, `fullJoin`. +Join conditions accept `eq()` or a nonempty, possibly nested `and()` of equalities (equality only -- IVM constraint). Joins take an imported operator: `.join(leftJoin(source, on))`, and likewise `innerJoin`, `rightJoin`, `fullJoin`, and `joinOf(type, source, on)`. The `.join(source, on)` call shape and the `.leftJoin()` methods are removed and throw `JoinOperatorRequiredError`. ```ts -import { eq } from '@tanstack/db' +import { eq, innerJoin } from '@tanstack/db' const userPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .innerJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(innerJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId))) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, @@ -137,15 +136,14 @@ Multiple joins: ```ts q.from({ user: usersCollection }) - .join({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId)) - .join({ comment: commentsCollection }, ({ post, comment }) => - eq(post.id, comment.postId), - ) + .join(leftJoin({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId))) + .join(leftJoin({ comment: commentsCollection }, ({ post, comment }) => + eq(post.id, comment.postId))) ``` ### 3. Aggregation with groupBy + having -Use `groupBy` to group rows, then aggregate in `select`. Filter groups with `having`. The `$selected` namespace lets `having` and `orderBy` reference fields defined in `select`. +Use `groupBy` to group rows, then aggregate in `select`. Filter groups with `having`. The `$selected` namespace lets `having` and `orderBy` reference fields defined in `select`. A `groupBy` with no aggregate wraps its keys in `groupKeys()`: `.groupBy(groupKeys(({ order }) => order.status))`; without it the query throws `GroupingOperatorRequiredError`. ```ts import { count, sum, gt } from '@tanstack/db' @@ -190,9 +188,8 @@ const activeUsers = createLiveQueryCollection((q) => const activeUserPosts = createLiveQueryCollection((q) => q .from({ user: activeUsers }) - .join({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ) + .join(leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId))) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, @@ -240,19 +237,19 @@ parent field with a child field. Return a child `Collection` on each parent row: ```ts -import { eq, createLiveQueryCollection } from '@tanstack/db' +import { createLiveQueryCollection, eq, include } from '@tanstack/db' const projectsWithIssues = createLiveQueryCollection((q) => q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q + issues: include(q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) .select(({ i }) => ({ id: i.id, title: i.title, - })), + }))), })), ) @@ -342,7 +339,7 @@ const messagesWithContent = createLiveQueryCollection((q) => Like `toArray()`, it must be a top-level value in `select()` and cannot be nested inside `coalesce()`, `eq()`, or another expression. - `concat(toArray())` requires a **scalar** `select` to concatenate into a string. -- Collection includes (bare subquery) require an **object** `select`. +- Collection includes (`include(subquery)`) require an **object** `select`. A bare subquery in `select` throws `IncludeOperatorRequiredError`. - Includes subqueries are compiled into the same incremental pipeline as the parent query -- they are not separate live queries. ## One-Shot Queries with queryOnce @@ -530,13 +527,11 @@ q.from({ order: ordersCollection }) ### Compound joins and acquisition ```ts -import { and, eq } from '@tanstack/db' +import { and, eq, leftJoin } from '@tanstack/db' -q.from({ product: productsCollection }).leftJoin( - { stock: stockCollection }, +q.from({ product: productsCollection }).join(leftJoin({ stock: stockCollection }, ({ product, stock }) => - and(eq(product.sku, stock.sku), eq(product.region, stock.region)), -) + and(eq(product.sku, stock.sku), eq(product.region, stock.region)))) ``` All equalities must match. A nullish component never matches, and outer joins @@ -568,16 +563,12 @@ The differential dataflow join operator supports a single equality or nested `an ```ts // WRONG -q.from({ user: usersCollection }).join( - { post: postsCollection }, - ({ user, post }) => gt(user.id, post.userId), -) +q.from({ user: usersCollection }).join(leftJoin({ post: postsCollection }, + ({ user, post }) => gt(user.id, post.userId))) // CORRECT -q.from({ user: usersCollection }).join( - { post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId), -) +q.from({ user: usersCollection }).join(leftJoin({ post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId))) ``` ### MEDIUM: Passing source directly instead of {alias: collection} diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index 15c198cc80..dc1391c654 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -314,9 +314,11 @@ export class BaseQueryBuilder { /** * Join another table or subquery to the current query * - * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery - * @param onCallback - A function that receives table references and returns the join condition - * @param type - The type of join: 'inner', 'left', 'right', or 'full' (defaults to 'left') + * @param operator - A join operator: `leftJoin`, `innerJoin`, `rightJoin`, + * `fullJoin`, or `joinOf(type, ...)`. Each takes an object with a single + * key-value pair (the alias and a Collection or subquery) and a callback + * that returns the join condition. Pass it inline so the callback is typed + * from this query. * @returns A QueryBuilder with the joined table available * * @example @@ -665,7 +667,9 @@ export class BaseQueryBuilder { /** * Group rows by one or more columns for aggregation * - * @param callback - A function that receives table references and returns the field(s) to group by + * @param keys - A function that receives table references and returns the + * field(s) to group by. A query with no aggregate wraps it in `groupKeys()`; + * aggregates such as `count()` bring the grouping code themselves. * @returns A QueryBuilder with grouping applied (enables aggregate functions in SELECT and HAVING) * * @example @@ -688,6 +692,12 @@ export class BaseQueryBuilder { * category: sales.category, * totalSales: sum(sales.amount) * })) + * + * // Group without an aggregate + * query + * .from({ orders: ordersCollection }) + * .groupBy(groupKeys(({orders}) => orders.status)) + * .select(({orders}) => ({ status: orders.status })) * ``` */ groupBy( diff --git a/packages/db/tests/conformance/suite.ts b/packages/db/tests/conformance/suite.ts index 38df7e0693..314d98418d 100644 --- a/packages/db/tests/conformance/suite.ts +++ b/packages/db/tests/conformance/suite.ts @@ -1,4 +1,3 @@ -import { describe, expect, it } from 'vitest' /** * Shared live-query conformance suite. * @@ -37,6 +36,7 @@ import { describe, expect, it } from 'vitest' * no-data, initial readiness over a lagging on-demand source, param recompilation, optimistic mutation, pre-created & config-object * inputs, error status, and order-only moves. */ +import { describe, expect, it } from 'vitest' import { include, leftJoin, diff --git a/packages/db/tests/db-client-preload-identity-oracle.test.ts b/packages/db/tests/db-client-preload-identity-oracle.test.ts index 89cb3634e7..778e371557 100644 --- a/packages/db/tests/db-client-preload-identity-oracle.test.ts +++ b/packages/db/tests/db-client-preload-identity-oracle.test.ts @@ -1,5 +1,3 @@ -import { describe, expect, it, vi } from 'vitest' -import { pooledLiveQueries } from '../src/query/pooled-live-query.js' /** * # A DbClient preload cannot substitute another source Collection * @@ -57,6 +55,8 @@ import { pooledLiveQueries } from '../src/query/pooled-live-query.js' * the observer's own row after preload. It does not model concurrent first * claims or arbitrary hydration orders. */ +import { describe, expect, it, vi } from 'vitest' +import { pooledLiveQueries } from '../src/query/pooled-live-query.js' import { DbClient, Query, diff --git a/packages/db/tests/initial-readiness-exposure.test.ts b/packages/db/tests/initial-readiness-exposure.test.ts index 8f25f79568..8217ca96d1 100644 --- a/packages/db/tests/initial-readiness-exposure.test.ts +++ b/packages/db/tests/initial-readiness-exposure.test.ts @@ -1,5 +1,3 @@ -import { describe, expect, it } from 'vitest' -import { pooledLiveQueries } from '../src/query/pooled-live-query.js' /** * Initial-query readiness exposure witnesses for core readers. * @@ -14,6 +12,8 @@ import { pooledLiveQueries } from '../src/query/pooled-live-query.js' * live-query path that frameworks use for `eq` filters, and * `toArrayWhenReady`. */ +import { describe, expect, it } from 'vitest' +import { pooledLiveQueries } from '../src/query/pooled-live-query.js' import { createCollection } from '../src/collection/index.js' import { createLiveQueryCollection, eq } from '../src/query/index.js' import { resolveLiveQueryValue } from '../src/live-query-options.js' diff --git a/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts b/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts index acd25c8e34..2be228ec11 100644 --- a/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts +++ b/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts @@ -1,4 +1,3 @@ -import { describe, expect, it, vi } from 'vitest' /** * # When may a live-query Collection start provider work? * @@ -46,6 +45,7 @@ import { describe, expect, it, vi } from 'vitest' * `DbClient` stream preloads, a subscriber that outlives cleanup, and reads * that wait for readiness. */ +import { describe, expect, it, vi } from 'vitest' import { BTreeIndex, DbClient, diff --git a/packages/db/tests/query/optimizer-semantics-oracle.test.ts b/packages/db/tests/query/optimizer-semantics-oracle.test.ts index e657ef2f47..33500588d0 100644 --- a/packages/db/tests/query/optimizer-semantics-oracle.test.ts +++ b/packages/db/tests/query/optimizer-semantics-oracle.test.ts @@ -1,11 +1,3 @@ -import { describe, expect, test } from 'vitest' -import { joinRuntime } from '../../src/query/compiler/join-runtime.js' -import { - createLiveQueryCollection, - innerJoin, - leftJoin, - rightJoin, -} from '../../src/query/index.js' /** * An outer predicate on an aggregate subquery's result must run after the * aggregate. Moving it to source rows can change or remove the result. @@ -29,6 +21,14 @@ import { * supplies a second formulation for matching and nonmatching controls. * The nested matching cases also reject an all-empty QueryRef implementation. */ +import { describe, expect, test } from 'vitest' +import { joinRuntime } from '../../src/query/compiler/join-runtime.js' +import { + createLiveQueryCollection, + innerJoin, + leftJoin, + rightJoin, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { optimizeQuery } from '../../src/query/optimizer.js' import { diff --git a/packages/db/tests/query/query-api-type-algebra.test-d.ts b/packages/db/tests/query/query-api-type-algebra.test-d.ts index 7de11c71d3..9dcd7f54c0 100644 --- a/packages/db/tests/query/query-api-type-algebra.test-d.ts +++ b/packages/db/tests/query/query-api-type-algebra.test-d.ts @@ -1,4 +1,3 @@ -import { describe, expectTypeOf, test } from 'vitest' /** * Oracle owner: the query-builder compile-time suites. * @@ -13,6 +12,7 @@ import { describe, expectTypeOf, test } from 'vitest' * paired runtime test owns the unmatched-row value witness. Existing generic, * join, and nullable-leaf suites remain the broader compatibility owners. */ +import { describe, expectTypeOf, test } from 'vitest' import { Query, createLiveQueryCollection, diff --git a/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts b/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts index 76297b2fae..c1c715145f 100644 --- a/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts +++ b/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts @@ -1,11 +1,3 @@ -import { describe, expectTypeOf, test } from 'vitest' -import { - createLiveQueryCollection, - eq, - leftJoin, - materialize, - toArray, -} from '../../src/query/index.js' /** * # Which types represent virtual-field-bearing rows? * @@ -41,6 +33,14 @@ import { * nested-helper cells and `GetRawResult` enrichment breaks eleven * projected-value cells. */ +import { describe, expectTypeOf, test } from 'vitest' +import { + createLiveQueryCollection, + eq, + leftJoin, + materialize, + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { RefLeaf } from '../../src/query/builder/types.js' diff --git a/packages/db/tests/query/virtual-row-fields-oracle.test.ts b/packages/db/tests/query/virtual-row-fields-oracle.test.ts index cdab607e8f..c2813fb78f 100644 --- a/packages/db/tests/query/virtual-row-fields-oracle.test.ts +++ b/packages/db/tests/query/virtual-row-fields-oracle.test.ts @@ -1,11 +1,3 @@ -import { describe, expect, test } from 'vitest' -import { - createLiveQueryCollection, - eq, - leftJoin, - materialize, - toArray, -} from '../../src/query/index.js' /** * # Which published values carry virtual row fields? * @@ -40,6 +32,14 @@ import { * observations. `checkWithCleanup` retains the primary mismatch as the cause * when Collection cleanup also fails and attempts every cleanup. */ +import { describe, expect, test } from 'vitest' +import { + createLiveQueryCollection, + eq, + leftJoin, + materialize, + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { hasVirtualProps } from '../../src/virtual-props.js' import { mockSyncCollectionOptions } from '../utils.js' diff --git a/packages/react-db/skills/react-db/SKILL.md b/packages/react-db/skills/react-db/SKILL.md index 562d9b3d36..a09895d0f6 100644 --- a/packages/react-db/skills/react-db/SKILL.md +++ b/packages/react-db/skills/react-db/SKILL.md @@ -100,6 +100,8 @@ const { data, status } = useLiveQuery({ // When disabled: status='disabled', data=undefined ``` +Many queries of one shape, such as one per list row, can share one partition. Pass `pool: pooledLiveQueries` (from `@tanstack/db`) in the config form: `useLiveQuery({ query, pool: pooledLiveQueries })`. Pooling applies to queries that filter one eager source with `eq(field, literal)`. It is off by default, and Suspense and `DbClient` queries never pool. + ### useLiveSuspenseQuery ```tsx @@ -171,7 +173,7 @@ const mutate = usePacedMutations({ ## Includes (Hierarchical Data) -When a query uses includes (subqueries in `select`), each child field is a live `Collection` by default. Subscribe to it with `useLiveQuery` in a subcomponent: +When a query uses `include()` for a subquery in `select`, each child field is a live `Collection`. Subscribe to it with `useLiveQuery` in a subcomponent: ```tsx function ProjectList() { @@ -180,10 +182,10 @@ function ProjectList() { q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q + issues: include(q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ id: i.id, title: i.title })), + .select(({ i }) => ({ id: i.id, title: i.title }))), })), }) diff --git a/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx b/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx index 167437846c..b834300049 100644 --- a/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx +++ b/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx @@ -1,17 +1,3 @@ -import { - DbClient, - Query, - collectionOptions, - createCollection, - eq, - getStableQueryBuilderHash, - innerJoin, - isUndefined, - joinOf, - leftJoin, - prepareLiveQueryValue, - toArray, -} from '@tanstack/db' /** * A Query assembled with a Collection descriptor is a reusable query plan. * Its source Collection belongs to the DbClient that later consumes it, so @@ -96,6 +82,20 @@ import { * start the source even though the earlier render never committed. These * histories do not cover arbitrary concurrent render schedules. */ +import { + DbClient, + Query, + collectionOptions, + createCollection, + eq, + getStableQueryBuilderHash, + innerJoin, + isUndefined, + joinOf, + leftJoin, + prepareLiveQueryValue, + toArray, +} from '@tanstack/db' import { act, render, renderHook, waitFor } from '@testing-library/react' import { describe, expect, it } from 'vitest' import { Suspense } from 'react' diff --git a/packages/react-db/tests/source-id-reuse-oracle.test.tsx b/packages/react-db/tests/source-id-reuse-oracle.test.tsx index 20769f9459..26edcd2533 100644 --- a/packages/react-db/tests/source-id-reuse-oracle.test.tsx +++ b/packages/react-db/tests/source-id-reuse-oracle.test.tsx @@ -1,11 +1,3 @@ -import { - DbClient, - collectionOptions, - createCollection, - eq, - gt, - leftJoin, -} from '@tanstack/db' /** * Source ID reuse contract for React's derived live-query identity. * @@ -36,6 +28,14 @@ import { * This does not assert a process-wide Collection registry, legacy dependency * arrays, or explicit-key semantics; explicit queryKey callers own their key. */ +import { + DbClient, + collectionOptions, + createCollection, + eq, + gt, + leftJoin, +} from '@tanstack/db' import { act, render, renderHook, waitFor } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' import { Component, Suspense } from 'react' diff --git a/packages/solid-db/skills/solid-db/SKILL.md b/packages/solid-db/skills/solid-db/SKILL.md index 0039c6cc89..5078d62e88 100644 --- a/packages/solid-db/skills/solid-db/SKILL.md +++ b/packages/solid-db/skills/solid-db/SKILL.md @@ -145,7 +145,7 @@ through the resource, so do not rely on reading `isError` after failure. ## Includes (Hierarchical Data) -When a query uses includes (subqueries in `select`), each child field is a live `Collection` by default. Subscribe to it with `useLiveQuery` in a subcomponent: +When a query uses `include()` for a subquery in `select`, each child field is a live `Collection`. Subscribe to it with `useLiveQuery` in a subcomponent: ```tsx function ProjectList() { @@ -153,10 +153,10 @@ function ProjectList() { q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: q + issues: include(q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ id: i.id, title: i.title })), + .select(({ i }) => ({ id: i.id, title: i.title }))), })), ) diff --git a/packages/svelte-db/skills/svelte-db/SKILL.md b/packages/svelte-db/skills/svelte-db/SKILL.md index 295103a302..ec92d3a161 100644 --- a/packages/svelte-db/skills/svelte-db/SKILL.md +++ b/packages/svelte-db/skills/svelte-db/SKILL.md @@ -113,7 +113,7 @@ const { data, isLoading } = $derived(query) ## Includes (Hierarchical Data) -When a query uses includes (subqueries in `select`), each child field is a live `Collection` by default. Subscribe to it with `useLiveQuery` in a subcomponent: +When a query uses `include()` for a subquery in `select`, each child field is a live `Collection`. Subscribe to it with `useLiveQuery` in a subcomponent: ```svelte diff --git a/packages/vue-db/skills/vue-db/SKILL.md b/packages/vue-db/skills/vue-db/SKILL.md index 3779be9a5d..5fe6f8c1f6 100644 --- a/packages/vue-db/skills/vue-db/SKILL.md +++ b/packages/vue-db/skills/vue-db/SKILL.md @@ -113,7 +113,7 @@ const handleToggle = (id: number) => { ## Includes (Hierarchical Data) -When a query uses includes (subqueries in `select`), each child field is a live `Collection` by default. Subscribe to it with `useLiveQuery` in a subcomponent: +When a query uses `include()` for a subquery in `select`, each child field is a live `Collection`. Subscribe to it with `useLiveQuery` in a subcomponent: ```vue From 4ecfdd8368939c8412fa88dd900b0f7e7a87adf9 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:47:05 +0000 Subject: [PATCH 14/32] ci: apply automated fixes --- .../angular-db/skills/angular-db/SKILL.md | 10 +-- .../db/skills/db-core/live-queries/SKILL.md | 65 +++++++++++++------ .../db/src/query/live/public-key-reduction.ts | 1 - packages/db/src/query/live/types.ts | 1 - packages/react-db/skills/react-db/SKILL.md | 10 +-- packages/solid-db/skills/solid-db/SKILL.md | 10 +-- 6 files changed, 62 insertions(+), 35 deletions(-) diff --git a/packages/angular-db/skills/angular-db/SKILL.md b/packages/angular-db/skills/angular-db/SKILL.md index b1f466b9e1..e7fdc38ff5 100644 --- a/packages/angular-db/skills/angular-db/SKILL.md +++ b/packages/angular-db/skills/angular-db/SKILL.md @@ -209,10 +209,12 @@ export class ProjectListComponent { q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: include(q - .from({ i: issuesCollection }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ id: i.id, title: i.title }))), + issues: include( + q + .from({ i: issuesCollection }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ id: i.id, title: i.title })), + ), })), ) } diff --git a/packages/db/skills/db-core/live-queries/SKILL.md b/packages/db/skills/db-core/live-queries/SKILL.md index 1b71f41900..504738462b 100644 --- a/packages/db/skills/db-core/live-queries/SKILL.md +++ b/packages/db/skills/db-core/live-queries/SKILL.md @@ -123,8 +123,11 @@ import { eq, innerJoin } from '@tanstack/db' const userPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(innerJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + innerJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, @@ -136,9 +139,16 @@ Multiple joins: ```ts q.from({ user: usersCollection }) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId))) - .join(leftJoin({ comment: commentsCollection }, ({ post, comment }) => - eq(post.id, comment.postId))) + .join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) + .join( + leftJoin({ comment: commentsCollection }, ({ post, comment }) => + eq(post.id, comment.postId), + ), + ) ``` ### 3. Aggregation with groupBy + having @@ -188,8 +198,11 @@ const activeUsers = createLiveQueryCollection((q) => const activeUserPosts = createLiveQueryCollection((q) => q .from({ user: activeUsers }) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, @@ -243,13 +256,15 @@ const projectsWithIssues = createLiveQueryCollection((q) => q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: include(q - .from({ i: issuesCollection }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - }))), + issues: include( + q + .from({ i: issuesCollection }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), })), ) @@ -529,9 +544,11 @@ q.from({ order: ordersCollection }) ```ts import { and, eq, leftJoin } from '@tanstack/db' -q.from({ product: productsCollection }).join(leftJoin({ stock: stockCollection }, - ({ product, stock }) => - and(eq(product.sku, stock.sku), eq(product.region, stock.region)))) +q.from({ product: productsCollection }).join( + leftJoin({ stock: stockCollection }, ({ product, stock }) => + and(eq(product.sku, stock.sku), eq(product.region, stock.region)), + ), +) ``` All equalities must match. A nullish component never matches, and outer joins @@ -563,12 +580,18 @@ The differential dataflow join operator supports a single equality or nested `an ```ts // WRONG -q.from({ user: usersCollection }).join(leftJoin({ post: postsCollection }, - ({ user, post }) => gt(user.id, post.userId))) +q.from({ user: usersCollection }).join( + leftJoin({ post: postsCollection }, ({ user, post }) => + gt(user.id, post.userId), + ), +) // CORRECT -q.from({ user: usersCollection }).join(leftJoin({ post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId))) +q.from({ user: usersCollection }).join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), +) ``` ### MEDIUM: Passing source directly instead of {alias: collection} diff --git a/packages/db/src/query/live/public-key-reduction.ts b/packages/db/src/query/live/public-key-reduction.ts index 90072b9bcd..5c4ffa332e 100644 --- a/packages/db/src/query/live/public-key-reduction.ts +++ b/packages/db/src/query/live/public-key-reduction.ts @@ -145,7 +145,6 @@ export function routeKey( ]) } - /** Reduces a query without includes to one canonical row per public key. */ export function reducePublicKeys( compilation: CompilationResult, diff --git a/packages/db/src/query/live/types.ts b/packages/db/src/query/live/types.ts index 1e09a30da5..f9dc5b6ece 100644 --- a/packages/db/src/query/live/types.ts +++ b/packages/db/src/query/live/types.ts @@ -86,7 +86,6 @@ export interface LiveQueryCollectionConfig< */ id?: string - /** * Query builder function that defines the live query */ diff --git a/packages/react-db/skills/react-db/SKILL.md b/packages/react-db/skills/react-db/SKILL.md index a09895d0f6..05cfbaa9bf 100644 --- a/packages/react-db/skills/react-db/SKILL.md +++ b/packages/react-db/skills/react-db/SKILL.md @@ -182,10 +182,12 @@ function ProjectList() { q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: include(q - .from({ i: issuesCollection }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ id: i.id, title: i.title }))), + issues: include( + q + .from({ i: issuesCollection }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ id: i.id, title: i.title })), + ), })), }) diff --git a/packages/solid-db/skills/solid-db/SKILL.md b/packages/solid-db/skills/solid-db/SKILL.md index 5078d62e88..f1feccf4e8 100644 --- a/packages/solid-db/skills/solid-db/SKILL.md +++ b/packages/solid-db/skills/solid-db/SKILL.md @@ -153,10 +153,12 @@ function ProjectList() { q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: include(q - .from({ i: issuesCollection }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ id: i.id, title: i.title }))), + issues: include( + q + .from({ i: issuesCollection }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ id: i.id, title: i.title })), + ), })), ) From a63707900088de2d3049557082c9d04940c5ce07 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 23 Oct 2026 09:26:30 -0600 Subject: [PATCH 15/32] fix(db): keep join conditions compiling without noImplicitAny A join operator infers the query's context from .join(). TypeScript skips that inference when noImplicitAny is off, which left every join condition with unknown refs and compile errors. The context now defaults to any there, so the condition is untyped like other unannotated callbacks in that mode. Strict mode keeps full typing. Co-authored-by: Isaac --- .../db/src/query/builder/join-operators.ts | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/db/src/query/builder/join-operators.ts b/packages/db/src/query/builder/join-operators.ts index b61465ea4d..be24180b25 100644 --- a/packages/db/src/query/builder/join-operators.ts +++ b/packages/db/src/query/builder/join-operators.ts @@ -29,15 +29,27 @@ export interface JoinOperator< /** A function that builds one join type's operator. */ export type JoinOperatorFunction = < - TContext extends Context, TSource extends Source, + TContext extends Context = any, >( source: TSource, - on: JoinOnCallback< - MergeContextForJoinCallback> - >, + on: JoinCondition, ) => JoinOperator +/** + * A join condition. `.join()` supplies the query's context; when TypeScript + * runs without noImplicitAny it skips that inference, and the condition is + * then untyped, as other unannotated callbacks are in that mode. + */ +export type JoinCondition< + TContext extends Context, + TSource extends Source, +> = 0 extends 1 & TContext + ? (refs: any) => any + : JoinOnCallback< + MergeContextForJoinCallback> + > + function joinOperator( type: TJoinType, ): JoinOperatorFunction { @@ -62,14 +74,12 @@ export const fullJoin: JoinOperatorFunction<`full`> = joinOperator(`full`) /** A join whose type is chosen at run time. */ export function joinOf< TJoinType extends JoinType, - TContext extends Context, TSource extends Source, + TContext extends Context = any, >( type: TJoinType, source: TSource, - on: JoinOnCallback< - MergeContextForJoinCallback> - >, + on: JoinCondition, ): JoinOperator { - return joinOperator(type)(source, on) + return joinOperator(type)(source, on) } From 003daac0763bda9d71579e6598961a61493f7604 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 23 Oct 2026 09:28:05 -0600 Subject: [PATCH 16/32] test(db): compile join conditions with noImplicitAny on and off Co-authored-by: Isaac --- .../tests/fixtures/loose-typescript-joins.ts | 34 ++++++++++++++++++ .../db/tests/join-typescript-modes.test.ts | 35 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 packages/db/tests/fixtures/loose-typescript-joins.ts create mode 100644 packages/db/tests/join-typescript-modes.test.ts diff --git a/packages/db/tests/fixtures/loose-typescript-joins.ts b/packages/db/tests/fixtures/loose-typescript-joins.ts new file mode 100644 index 0000000000..030bc9ea00 --- /dev/null +++ b/packages/db/tests/fixtures/loose-typescript-joins.ts @@ -0,0 +1,34 @@ +// Compiled by join-typescript-modes.test.ts with noImplicitAny on and off. +import { + createLiveQueryCollection, + eq, + innerJoin, + joinOf, + leftJoin, +} from '../../src/index.js' +import type { Collection } from '../../src/index.js' + +type User = { id: number; name: string } +type Post = { id: number; userId: number; title: string } +declare const users: Collection +declare const posts: Collection +declare const type: `left` | `inner` + +export const joined = createLiveQueryCollection((q) => + q + .from({ user: users }) + .join( + leftJoin({ post: posts }, ({ user, post }) => eq(user.id, post.userId)), + ) + .join( + innerJoin({ again: posts }, ({ user, again }) => + eq(user.id, again.userId), + ), + ) + .join( + joinOf(type, { third: posts }, ({ user, third }) => + eq(user.id, third.userId), + ), + ) + .select(({ user, post }) => ({ name: user.name, title: post.title })), +) diff --git a/packages/db/tests/join-typescript-modes.test.ts b/packages/db/tests/join-typescript-modes.test.ts new file mode 100644 index 0000000000..8ff91e1295 --- /dev/null +++ b/packages/db/tests/join-typescript-modes.test.ts @@ -0,0 +1,35 @@ +// @vitest-environment node +import { fileURLToPath } from 'node:url' +import ts from 'typescript' +import { describe, expect, it } from 'vitest' + +// A join operator gets the query's context from `.join()` by inference. +// TypeScript skips that inference without noImplicitAny, so a join condition +// must still compile there, untyped like other callbacks in that mode. +// Strict mode must keep it typed. Type tests run in strict mode only, so this +// compiles one fixture in both modes. +const fixture = fileURLToPath( + new URL(`./fixtures/loose-typescript-joins.ts`, import.meta.url), +) +const config = ts.getParsedCommandLineOfConfigFile( + fileURLToPath(new URL(`../tsconfig.json`, import.meta.url)), + {}, + { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, +)! + +function errors(noImplicitAny: boolean) { + const program = ts.createProgram([fixture], { + ...config.options, + noImplicitAny, + noEmit: true, + }) + return ts + .getPreEmitDiagnostics(program, program.getSourceFile(fixture)) + .map((d) => ts.flattenDiagnosticMessageText(d.messageText, ` `)) +} + +describe(`join conditions across TypeScript modes`, () => { + it.each([true, false])(`compile with noImplicitAny %s`, (noImplicitAny) => { + expect(errors(noImplicitAny)).toEqual([]) + }) +}) From 938ac833bb92cd50b54754a609b633619699aa9c Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 23 Oct 2026 09:58:33 -0600 Subject: [PATCH 17/32] test: typecheck doc and skill examples; reject bare subqueries in select Add scripts/check-doc-snippets.mjs, run in the CI lint job as `pnpm test:doc-snippets`. It typechecks each TypeScript example in docs/ and the shipped skills against the workspace sources. Examples that already failed are listed in a baseline by content hash, so a new or edited example must pass. The check found these errors: - Joins lost their types without noImplicitAny. Strict mode keeps them; the test compiles a wrong field in both modes. - A bare subquery in select type-checked, also nested, because the ref and nested-object shapes are open. Both select overloads now reject a value that holds a query builder outside include(), toArray(), or materialize(). - Guide and skill examples: a nested bare subquery, missing imports, an orderBy on $selected, and iteration over a Collection. Co-authored-by: Isaac --- .github/workflows/pr.yml | 2 + docs/guides/live-queries.md | 10 +- docs/reference/classes/BaseQueryBuilder.md | 40 +-- docs/reference/functions/add.md | 2 +- docs/reference/functions/avg.md | 6 +- docs/reference/functions/caseWhen.md | 110 +++--- docs/reference/functions/count.md | 2 +- docs/reference/functions/divide.md | 2 +- docs/reference/functions/joinOf.md | 14 +- docs/reference/functions/max.md | 6 +- docs/reference/functions/min.md | 6 +- docs/reference/functions/multiply.md | 2 +- docs/reference/functions/subtract.md | 2 +- docs/reference/functions/sum.md | 6 +- .../ApplyJoinOptionalityToMergedSchema.md | 2 +- docs/reference/type-aliases/ExtractContext.md | 2 +- .../type-aliases/FunctionalHavingRow.md | 2 +- docs/reference/type-aliases/GetResult.md | 2 +- .../reference/type-aliases/GroupByCallback.md | 2 +- .../reference/type-aliases/InferResultType.md | 2 +- .../type-aliases/InitialQueryBuilder.md | 2 +- docs/reference/type-aliases/JoinOnCallback.md | 2 +- .../MergeContextForJoinCallback.md | 2 +- .../type-aliases/MergeContextWithJoinType.md | 2 +- docs/reference/type-aliases/OperatorName.md | 2 +- .../reference/type-aliases/OrderByCallback.md | 2 +- docs/reference/type-aliases/Prettify.md | 2 +- docs/reference/type-aliases/QueryBuilder.md | 2 +- docs/reference/type-aliases/QueryResult.md | 2 +- docs/reference/type-aliases/Ref.md | 2 +- docs/reference/type-aliases/RefsForContext.md | 2 +- .../type-aliases/ResultTypeFromSelect.md | 2 +- docs/reference/type-aliases/WithResult.md | 2 +- docs/reference/variables/Query.md | 2 +- docs/reference/variables/fullJoin.md | 2 +- docs/reference/variables/innerJoin.md | 2 +- docs/reference/variables/leftJoin.md | 2 +- docs/reference/variables/operators.md | 2 +- docs/reference/variables/rightJoin.md | 2 +- package.json | 1 + .../db/skills/db-core/live-queries/SKILL.md | 69 ++-- packages/db/src/query/builder/functions.ts | 8 +- packages/db/src/query/builder/index.ts | 5 +- packages/db/src/query/builder/types.ts | 28 +- .../db/tests/join-typescript-modes.test.ts | 57 ++- packages/db/tests/query/includes.test-d.ts | 21 ++ scripts/check-doc-snippets.mjs | 334 ++++++++++++++++++ scripts/doc-snippets-baseline.json | 273 ++++++++++++++ 48 files changed, 899 insertions(+), 157 deletions(-) create mode 100644 scripts/check-doc-snippets.mjs create mode 100644 scripts/doc-snippets-baseline.json diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b5eb9dd20d..1be0992de5 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -26,6 +26,8 @@ jobs: uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 - name: Check lint run: pnpm run lint:check + - name: Typecheck doc and skill examples + run: pnpm run test:doc-snippets minified-db: name: Minified DB public API runs-on: ubuntu-latest diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index 5ff9cc16af..f3fe9bebf6 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -1192,7 +1192,7 @@ In this example, the `activeUsers` subquery is used twice but executed only once Build complex queries with multiple levels of nesting: ```ts -import { count, leftJoin } from '@tanstack/db' +import { count, createCollection, eq, leftJoin, liveQueryCollectionOptions } from '@tanstack/db' const topUsers = createCollection(liveQueryCollectionOptions({ query: (q) => { @@ -1216,7 +1216,7 @@ const topUsers = createCollection(liveQueryCollectionOptions({ name: user.name, postCount: postCount.count, })) - .orderBy(({ userStats }) => userStats.postCount, 'desc') + .orderBy(({ $selected }) => $selected.postCount, 'desc') .limit(10) // Use the user stats subquery in the main query @@ -1419,13 +1419,13 @@ const tree = createLiveQueryCollection((q) => .select(({ i }) => ({ id: i.id, title: i.title, - comments: q + comments: include(q .from({ c: commentsCollection }) .where(({ c }) => eq(c.issueId, i.id)) .select(({ c }) => ({ id: c.id, body: c.body, - })), + }))), }))), })), ) @@ -2121,7 +2121,7 @@ const activeUserPosts = createLiveQueryCollection((q) => You can use the `Query` class to create reusable query definitions. This is useful for building complex queries where you want to reuse the same query builder instance multiple times throughout your application. ```ts -import { eq, leftJoin, Query } from '@tanstack/db' +import { createLiveQueryCollection, eq, leftJoin, Query } from '@tanstack/db' // Create a reusable query builder const userQuery = new Query() diff --git a/docs/reference/classes/BaseQueryBuilder.md b/docs/reference/classes/BaseQueryBuilder.md index 2d5e783f40..11bc7c0789 100644 --- a/docs/reference/classes/BaseQueryBuilder.md +++ b/docs/reference/classes/BaseQueryBuilder.md @@ -3,7 +3,7 @@ id: BaseQueryBuilder title: BaseQueryBuilder --- -Defined in: [packages/db/src/query/builder/index.ts:142](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L142) +Defined in: [packages/db/src/query/builder/index.ts:143](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L143) ## Type Parameters @@ -19,7 +19,7 @@ Defined in: [packages/db/src/query/builder/index.ts:142](https://github.com/TanS new BaseQueryBuilder(query, owned): BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:145](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L145) +Defined in: [packages/db/src/query/builder/index.ts:146](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L146) #### Parameters @@ -49,7 +49,7 @@ Whether the builder may keep `query` without copying it. get fn(): object; ``` -Defined in: [packages/db/src/query/builder/index.ts:871](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L871) +Defined in: [packages/db/src/query/builder/index.ts:874](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L874) Functional variants of the query builder These are imperative function that are called for ery row. @@ -192,7 +192,7 @@ query _bindCollectionSources(resolveCollection): BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:976](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L976) +Defined in: [packages/db/src/query/builder/index.ts:979](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L979) Bind this query for one consumer. Later descriptor clauses wait for another consumer. @@ -214,7 +214,7 @@ Bind this query for one consumer. Later descriptor clauses wait for another cons _getQuery(): QueryIR; ``` -Defined in: [packages/db/src/query/builder/index.ts:968](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L968) +Defined in: [packages/db/src/query/builder/index.ts:971](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L971) #### Returns @@ -228,7 +228,7 @@ Defined in: [packages/db/src/query/builder/index.ts:968](https://github.com/TanS distinct(): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:788](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L788) +Defined in: [packages/db/src/query/builder/index.ts:791](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L791) Specify that the query should return distinct rows. Deduplicates rows based on the selected columns. @@ -257,7 +257,7 @@ query findOne(): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:808](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L808) +Defined in: [packages/db/src/query/builder/index.ts:811](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L811) Specify that the query should return a single result @@ -285,7 +285,7 @@ query from(source): QueryBuilder>; ``` -Defined in: [packages/db/src/query/builder/index.ts:252](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L252) +Defined in: [packages/db/src/query/builder/index.ts:253](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L253) Specify the source table or subquery for the query @@ -328,7 +328,7 @@ query.from({ activeUsers }) groupBy(keys): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:703](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L703) +Defined in: [packages/db/src/query/builder/index.ts:706](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L706) Group rows by one or more columns for aggregation @@ -385,7 +385,7 @@ query having(callback): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:478](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L478) +Defined in: [packages/db/src/query/builder/index.ts:479](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L479) Filter grouped rows based on aggregate conditions @@ -434,7 +434,7 @@ query join(operator): QueryBuilder, TJoinType>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:342](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L342) +Defined in: [packages/db/src/query/builder/index.ts:343](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L343) Join another table or subquery to the current query @@ -493,7 +493,7 @@ query limit(count): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:743](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L743) +Defined in: [packages/db/src/query/builder/index.ts:746](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L746) Limit the number of rows returned by the query `orderBy` is required for `limit` @@ -530,7 +530,7 @@ query offset(count): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:767](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L767) +Defined in: [packages/db/src/query/builder/index.ts:770](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L770) Skip a number of rows before returning results `orderBy` is required for `offset` @@ -568,7 +568,7 @@ query orderBy(callback, options): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:610](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L610) +Defined in: [packages/db/src/query/builder/index.ts:613](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L613) Sort the query results by one or more columns @@ -620,7 +620,7 @@ query select(callback): QueryBuilder>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:544](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L544) +Defined in: [packages/db/src/query/builder/index.ts:545](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L545) Select specific columns or computed values from the query @@ -679,7 +679,7 @@ query select(callback): QueryBuilder>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:549](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L549) +Defined in: [packages/db/src/query/builder/index.ts:550](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L550) Select specific columns or computed values from the query @@ -693,7 +693,7 @@ Select specific columns or computed values from the query ###### callback -(`refs`) => `TSelectValue` +(`refs`) => `WithoutBareSubquery`\<`TSelectValue`\> A function that receives table references and returns an object with selected fields or expressions @@ -742,7 +742,7 @@ query unionAll(source): QueryBuilder>; ``` -Defined in: [packages/db/src/query/builder/index.ts:278](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L278) +Defined in: [packages/db/src/query/builder/index.ts:279](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L279) Union multiple independent source streams in one query. @@ -782,7 +782,7 @@ query unionAll(...branches): QueryBuilder>; ``` -Defined in: [packages/db/src/query/builder/index.ts:281](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L281) +Defined in: [packages/db/src/query/builder/index.ts:282](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L282) Union multiple independent source streams in one query. @@ -822,7 +822,7 @@ query where(callback): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:420](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L420) +Defined in: [packages/db/src/query/builder/index.ts:421](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L421) Filter rows based on a condition diff --git a/docs/reference/functions/add.md b/docs/reference/functions/add.md index 2bb2314897..8438906228 100644 --- a/docs/reference/functions/add.md +++ b/docs/reference/functions/add.md @@ -7,7 +7,7 @@ title: add function add(left, right): BinaryNumericReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:615](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L615) +Defined in: [packages/db/src/query/builder/functions.ts:617](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L617) ## Type Parameters diff --git a/docs/reference/functions/avg.md b/docs/reference/functions/avg.md index c3b5da604e..3eafd3d5c2 100644 --- a/docs/reference/functions/avg.md +++ b/docs/reference/functions/avg.md @@ -9,7 +9,7 @@ title: avg function avg(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:661](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L661) +Defined in: [packages/db/src/query/builder/functions.ts:663](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L663) ### Type Parameters @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:661](https://github.com/ function avg(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:662](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L662) +Defined in: [packages/db/src/query/builder/functions.ts:664](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L664) ### Type Parameters @@ -57,7 +57,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:662](https://github.com/ function avg(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:665](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L665) +Defined in: [packages/db/src/query/builder/functions.ts:667](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L667) ### Type Parameters diff --git a/docs/reference/functions/caseWhen.md b/docs/reference/functions/caseWhen.md index d7e6227612..607c3dc771 100644 --- a/docs/reference/functions/caseWhen.md +++ b/docs/reference/functions/caseWhen.md @@ -9,7 +9,7 @@ title: caseWhen function caseWhen(condition1, value1): CaseWhenResult<[V1], false>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:405](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L405) +Defined in: [packages/db/src/query/builder/functions.ts:407](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L407) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -68,9 +68,11 @@ caseWhen( ```ts caseWhen(gt(user.age, 18), { ...user, - posts: q - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)), + posts: include( + q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), + ), }) ``` @@ -83,7 +85,7 @@ function caseWhen( defaultValue): CaseWhenResult<[V1, D], true>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:409](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L409) +Defined in: [packages/db/src/query/builder/functions.ts:411](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L411) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -150,9 +152,11 @@ caseWhen( ```ts caseWhen(gt(user.age, 18), { ...user, - posts: q - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)), + posts: include( + q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), + ), }) ``` @@ -166,7 +170,7 @@ function caseWhen( value2): CaseWhenResult<[V1, V2], false>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:414](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L414) +Defined in: [packages/db/src/query/builder/functions.ts:416](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L416) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -241,9 +245,11 @@ caseWhen( ```ts caseWhen(gt(user.age, 18), { ...user, - posts: q - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)), + posts: include( + q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), + ), }) ``` @@ -258,7 +264,7 @@ function caseWhen( defaultValue): CaseWhenResult<[V1, V2, D], true>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:425](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L425) +Defined in: [packages/db/src/query/builder/functions.ts:427](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L427) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -341,9 +347,11 @@ caseWhen( ```ts caseWhen(gt(user.age, 18), { ...user, - posts: q - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)), + posts: include( + q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), + ), }) ``` @@ -359,7 +367,7 @@ function caseWhen( value3): CaseWhenResult<[V1, V2, V3], false>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:438](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L438) +Defined in: [packages/db/src/query/builder/functions.ts:440](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L440) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -450,9 +458,11 @@ caseWhen( ```ts caseWhen(gt(user.age, 18), { ...user, - posts: q - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)), + posts: include( + q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), + ), }) ``` @@ -469,7 +479,7 @@ function caseWhen( defaultValue): CaseWhenResult<[V1, V2, V3, D], true>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:453](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L453) +Defined in: [packages/db/src/query/builder/functions.ts:455](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L455) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -568,9 +578,11 @@ caseWhen( ```ts caseWhen(gt(user.age, 18), { ...user, - posts: q - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)), + posts: include( + q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), + ), }) ``` @@ -588,7 +600,7 @@ function caseWhen( value4): CaseWhenResult<[V1, V2, V3, V4], false>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:470](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L470) +Defined in: [packages/db/src/query/builder/functions.ts:472](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L472) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -695,9 +707,11 @@ caseWhen( ```ts caseWhen(gt(user.age, 18), { ...user, - posts: q - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)), + posts: include( + q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), + ), }) ``` @@ -716,7 +730,7 @@ function caseWhen( defaultValue): CaseWhenResult<[V1, V2, V3, V4, D], true>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:489](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L489) +Defined in: [packages/db/src/query/builder/functions.ts:491](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L491) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -831,9 +845,11 @@ caseWhen( ```ts caseWhen(gt(user.age, 18), { ...user, - posts: q - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)), + posts: include( + q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), + ), }) ``` @@ -853,7 +869,7 @@ function caseWhen( value5): CaseWhenResult<[V1, V2, V3, V4, V5], false>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:510](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L510) +Defined in: [packages/db/src/query/builder/functions.ts:512](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L512) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -976,9 +992,11 @@ caseWhen( ```ts caseWhen(gt(user.age, 18), { ...user, - posts: q - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)), + posts: include( + q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), + ), }) ``` @@ -999,7 +1017,7 @@ function caseWhen( defaultValue): CaseWhenResult<[V1, V2, V3, V4, V5, D], true>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:533](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L533) +Defined in: [packages/db/src/query/builder/functions.ts:535](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L535) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -1130,9 +1148,11 @@ caseWhen( ```ts caseWhen(gt(user.age, 18), { ...user, - posts: q - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)), + posts: include( + q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), + ), }) ``` @@ -1155,7 +1175,7 @@ function caseWhen( rest): any; ``` -Defined in: [packages/db/src/query/builder/functions.ts:558](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L558) +Defined in: [packages/db/src/query/builder/functions.ts:560](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L560) Returns the value for the first matching condition, similar to SQL `CASE WHEN`. @@ -1290,8 +1310,10 @@ caseWhen( ```ts caseWhen(gt(user.age, 18), { ...user, - posts: q - .from({ post: postsCollection }) - .where(({ post }) => eq(post.userId, user.id)), + posts: include( + q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), + ), }) ``` diff --git a/docs/reference/functions/count.md b/docs/reference/functions/count.md index 8f0679f785..bf29d462b9 100644 --- a/docs/reference/functions/count.md +++ b/docs/reference/functions/count.md @@ -7,7 +7,7 @@ title: count function count(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:657](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L657) +Defined in: [packages/db/src/query/builder/functions.ts:659](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L659) ## Parameters diff --git a/docs/reference/functions/divide.md b/docs/reference/functions/divide.md index 49911fab34..5ebebab1dd 100644 --- a/docs/reference/functions/divide.md +++ b/docs/reference/functions/divide.md @@ -7,7 +7,7 @@ title: divide function divide(left, right): DivideReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:645](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L645) +Defined in: [packages/db/src/query/builder/functions.ts:647](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L647) ## Type Parameters diff --git a/docs/reference/functions/joinOf.md b/docs/reference/functions/joinOf.md index 011778a2b2..9c9f0e8975 100644 --- a/docs/reference/functions/joinOf.md +++ b/docs/reference/functions/joinOf.md @@ -4,13 +4,13 @@ title: joinOf --- ```ts -function joinOf( +function joinOf( type, source, on): JoinOperator; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:63](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L63) +Defined in: [packages/db/src/query/builder/join-operators.ts:75](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L75) A join whose type is chosen at run time. @@ -20,14 +20,14 @@ A join whose type is chosen at run time. `TJoinType` *extends* [`JoinType`](../type-aliases/JoinType.md) -### TContext - -`TContext` *extends* [`Context`](../interfaces/Context.md) - ### TSource `TSource` *extends* [`Source`](../type-aliases/Source.md) +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) = `any` + ## Parameters ### type @@ -40,7 +40,7 @@ A join whose type is chosen at run time. ### on -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> +`JoinCondition`\<`TContext`, `TSource`\> ## Returns diff --git a/docs/reference/functions/max.md b/docs/reference/functions/max.md index 03ac6b93fd..7de95c31b8 100644 --- a/docs/reference/functions/max.md +++ b/docs/reference/functions/max.md @@ -9,7 +9,7 @@ title: max function max(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:692](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L692) +Defined in: [packages/db/src/query/builder/functions.ts:694](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L694) ### Type Parameters @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:692](https://github.com/ function max(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:693](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L693) +Defined in: [packages/db/src/query/builder/functions.ts:695](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L695) ### Type Parameters @@ -57,7 +57,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:693](https://github.com/ function max(arg): Aggregate>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:694](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L694) +Defined in: [packages/db/src/query/builder/functions.ts:696](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L696) ### Type Parameters diff --git a/docs/reference/functions/min.md b/docs/reference/functions/min.md index af5d19b1b8..ba8050e06d 100644 --- a/docs/reference/functions/min.md +++ b/docs/reference/functions/min.md @@ -9,7 +9,7 @@ title: min function min(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:683](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L683) +Defined in: [packages/db/src/query/builder/functions.ts:685](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L685) ### Type Parameters @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:683](https://github.com/ function min(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:684](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L684) +Defined in: [packages/db/src/query/builder/functions.ts:686](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L686) ### Type Parameters @@ -57,7 +57,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:684](https://github.com/ function min(arg): Aggregate>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:685](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L685) +Defined in: [packages/db/src/query/builder/functions.ts:687](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L687) ### Type Parameters diff --git a/docs/reference/functions/multiply.md b/docs/reference/functions/multiply.md index be91130f70..6408bcdd27 100644 --- a/docs/reference/functions/multiply.md +++ b/docs/reference/functions/multiply.md @@ -7,7 +7,7 @@ title: multiply function multiply(left, right): BinaryNumericReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:635](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L635) +Defined in: [packages/db/src/query/builder/functions.ts:637](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L637) ## Type Parameters diff --git a/docs/reference/functions/subtract.md b/docs/reference/functions/subtract.md index 10a86945f9..07d4ea7604 100644 --- a/docs/reference/functions/subtract.md +++ b/docs/reference/functions/subtract.md @@ -7,7 +7,7 @@ title: subtract function subtract(left, right): BinaryNumericReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:625](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L625) +Defined in: [packages/db/src/query/builder/functions.ts:627](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L627) ## Type Parameters diff --git a/docs/reference/functions/sum.md b/docs/reference/functions/sum.md index fb700a051f..bdffa67f26 100644 --- a/docs/reference/functions/sum.md +++ b/docs/reference/functions/sum.md @@ -9,7 +9,7 @@ title: sum function sum(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:672](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L672) +Defined in: [packages/db/src/query/builder/functions.ts:674](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L674) ### Type Parameters @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:672](https://github.com/ function sum(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:673](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L673) +Defined in: [packages/db/src/query/builder/functions.ts:675](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L675) ### Type Parameters @@ -57,7 +57,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:673](https://github.com/ function sum(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:676](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L676) +Defined in: [packages/db/src/query/builder/functions.ts:678](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L678) ### Type Parameters diff --git a/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md b/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md index bbec18bb8b..be00b93665 100644 --- a/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md +++ b/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md @@ -7,7 +7,7 @@ title: ApplyJoinOptionalityToMergedSchema type ApplyJoinOptionalityToMergedSchema = { [K in keyof TExistingSchema]: K extends TFromSourceNames ? TJoinType extends "right" | "full" ? TExistingSchema[K] | undefined : TExistingSchema[K] : TExistingSchema[K] } & { [K in keyof TNewSchema]: TJoinType extends "left" | "full" ? TNewSchema[K] | undefined : TNewSchema[K] }; ``` -Defined in: [packages/db/src/query/builder/types.ts:1038](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1038) +Defined in: [packages/db/src/query/builder/types.ts:1064](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1064) ApplyJoinOptionalityToMergedSchema - Applies optionality rules when merging schemas diff --git a/docs/reference/type-aliases/ExtractContext.md b/docs/reference/type-aliases/ExtractContext.md index c3fb4fcb15..7c5a4ec5a9 100644 --- a/docs/reference/type-aliases/ExtractContext.md +++ b/docs/reference/type-aliases/ExtractContext.md @@ -7,7 +7,7 @@ title: ExtractContext type ExtractContext = T extends BaseQueryBuilder ? TContext : T extends QueryBuilder ? TContext : never; ``` -Defined in: [packages/db/src/query/builder/index.ts:1788](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1788) +Defined in: [packages/db/src/query/builder/index.ts:1791](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1791) ## Type Parameters diff --git a/docs/reference/type-aliases/FunctionalHavingRow.md b/docs/reference/type-aliases/FunctionalHavingRow.md index a3ea02343b..566ccfbd84 100644 --- a/docs/reference/type-aliases/FunctionalHavingRow.md +++ b/docs/reference/type-aliases/FunctionalHavingRow.md @@ -7,7 +7,7 @@ title: FunctionalHavingRow type FunctionalHavingRow = TContext["schema"] & TContext["hasResult"] extends true ? object : object; ``` -Defined in: [packages/db/src/query/builder/types.ts:663](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L663) +Defined in: [packages/db/src/query/builder/types.ts:689](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L689) FunctionalHavingRow - Type for the row parameter in functional having callbacks diff --git a/docs/reference/type-aliases/GetResult.md b/docs/reference/type-aliases/GetResult.md index 6bb3175ef3..966adb6430 100644 --- a/docs/reference/type-aliases/GetResult.md +++ b/docs/reference/type-aliases/GetResult.md @@ -7,7 +7,7 @@ title: GetResult type GetResult = Prettify>; ``` -Defined in: [packages/db/src/query/builder/types.ts:1172](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1172) +Defined in: [packages/db/src/query/builder/types.ts:1198](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1198) ## Type Parameters diff --git a/docs/reference/type-aliases/GroupByCallback.md b/docs/reference/type-aliases/GroupByCallback.md index 66d5cfd4d0..856824c727 100644 --- a/docs/reference/type-aliases/GroupByCallback.md +++ b/docs/reference/type-aliases/GroupByCallback.md @@ -7,7 +7,7 @@ title: GroupByCallback type GroupByCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:626](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L626) +Defined in: [packages/db/src/query/builder/types.ts:652](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L652) GroupByCallback - Type for groupBy clause callback functions diff --git a/docs/reference/type-aliases/InferResultType.md b/docs/reference/type-aliases/InferResultType.md index fea31ee06c..44683c1d2e 100644 --- a/docs/reference/type-aliases/InferResultType.md +++ b/docs/reference/type-aliases/InferResultType.md @@ -7,7 +7,7 @@ title: InferResultType type InferResultType = TContext extends SingleResult ? GetResult | undefined : GetResult[]; ``` -Defined in: [packages/db/src/query/builder/types.ts:1064](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1064) +Defined in: [packages/db/src/query/builder/types.ts:1090](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1090) Utility type to infer the query result size (single row or an array) diff --git a/docs/reference/type-aliases/InitialQueryBuilder.md b/docs/reference/type-aliases/InitialQueryBuilder.md index d7f4ff9e1c..46fa9d5627 100644 --- a/docs/reference/type-aliases/InitialQueryBuilder.md +++ b/docs/reference/type-aliases/InitialQueryBuilder.md @@ -7,4 +7,4 @@ title: InitialQueryBuilder type InitialQueryBuilder = Pick, "from" | "unionAll">; ``` -Defined in: [packages/db/src/query/builder/index.ts:1771](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1771) +Defined in: [packages/db/src/query/builder/index.ts:1774](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1774) diff --git a/docs/reference/type-aliases/JoinOnCallback.md b/docs/reference/type-aliases/JoinOnCallback.md index de3428e372..b4980cbef1 100644 --- a/docs/reference/type-aliases/JoinOnCallback.md +++ b/docs/reference/type-aliases/JoinOnCallback.md @@ -7,7 +7,7 @@ title: JoinOnCallback type JoinOnCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:642](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L642) +Defined in: [packages/db/src/query/builder/types.ts:668](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L668) JoinOnCallback - Type for join condition callback functions diff --git a/docs/reference/type-aliases/MergeContextForJoinCallback.md b/docs/reference/type-aliases/MergeContextForJoinCallback.md index fe7f9ab8f1..a9598cc9f4 100644 --- a/docs/reference/type-aliases/MergeContextForJoinCallback.md +++ b/docs/reference/type-aliases/MergeContextForJoinCallback.md @@ -7,7 +7,7 @@ title: MergeContextForJoinCallback type MergeContextForJoinCallback = Omit & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:1329](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1329) +Defined in: [packages/db/src/query/builder/types.ts:1355](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1355) MergeContextForJoinCallback - Special context for join condition callbacks diff --git a/docs/reference/type-aliases/MergeContextWithJoinType.md b/docs/reference/type-aliases/MergeContextWithJoinType.md index dbb5e5bbca..86b1de39a4 100644 --- a/docs/reference/type-aliases/MergeContextWithJoinType.md +++ b/docs/reference/type-aliases/MergeContextWithJoinType.md @@ -7,7 +7,7 @@ title: MergeContextWithJoinType type MergeContextWithJoinType = Omit & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:984](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L984) +Defined in: [packages/db/src/query/builder/types.ts:1010](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1010) MergeContextWithJoinType - Creates a new context after a join operation diff --git a/docs/reference/type-aliases/OperatorName.md b/docs/reference/type-aliases/OperatorName.md index 7074f85e05..444e6a6df4 100644 --- a/docs/reference/type-aliases/OperatorName.md +++ b/docs/reference/type-aliases/OperatorName.md @@ -7,4 +7,4 @@ title: OperatorName type OperatorName = typeof operators[number]; ``` -Defined in: [packages/db/src/query/builder/functions.ts:760](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L760) +Defined in: [packages/db/src/query/builder/functions.ts:762](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L762) diff --git a/docs/reference/type-aliases/OrderByCallback.md b/docs/reference/type-aliases/OrderByCallback.md index a45c3fee41..18c787bbd6 100644 --- a/docs/reference/type-aliases/OrderByCallback.md +++ b/docs/reference/type-aliases/OrderByCallback.md @@ -7,7 +7,7 @@ title: OrderByCallback type OrderByCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:590](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L590) +Defined in: [packages/db/src/query/builder/types.ts:616](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L616) OrderByCallback - Type for orderBy clause callback functions diff --git a/docs/reference/type-aliases/Prettify.md b/docs/reference/type-aliases/Prettify.md index 9422fdce71..9cf9678865 100644 --- a/docs/reference/type-aliases/Prettify.md +++ b/docs/reference/type-aliases/Prettify.md @@ -7,7 +7,7 @@ title: Prettify type Prettify = { [K in keyof T]: T[K] } & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:1371](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1371) +Defined in: [packages/db/src/query/builder/types.ts:1397](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1397) Prettify - Utility type for clean IDE display diff --git a/docs/reference/type-aliases/QueryBuilder.md b/docs/reference/type-aliases/QueryBuilder.md index 6fe434406b..c0ff115b32 100644 --- a/docs/reference/type-aliases/QueryBuilder.md +++ b/docs/reference/type-aliases/QueryBuilder.md @@ -7,7 +7,7 @@ title: QueryBuilder type QueryBuilder = Omit, "from" | "unionAll" | "_getQuery">; ``` -Defined in: [packages/db/src/query/builder/index.ts:1778](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1778) +Defined in: [packages/db/src/query/builder/index.ts:1781](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1781) ## Type Parameters diff --git a/docs/reference/type-aliases/QueryResult.md b/docs/reference/type-aliases/QueryResult.md index cc8180623f..d71e624d2d 100644 --- a/docs/reference/type-aliases/QueryResult.md +++ b/docs/reference/type-aliases/QueryResult.md @@ -7,7 +7,7 @@ title: QueryResult type QueryResult = GetResult>; ``` -Defined in: [packages/db/src/query/builder/index.ts:1796](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1796) +Defined in: [packages/db/src/query/builder/index.ts:1799](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1799) ## Type Parameters diff --git a/docs/reference/type-aliases/Ref.md b/docs/reference/type-aliases/Ref.md index dbc8d4025e..ab4b12b281 100644 --- a/docs/reference/type-aliases/Ref.md +++ b/docs/reference/type-aliases/Ref.md @@ -7,7 +7,7 @@ title: Ref type Ref = T extends unknown ? RefBranch : never; ``` -Defined in: [packages/db/src/query/builder/types.ts:900](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L900) +Defined in: [packages/db/src/query/builder/types.ts:926](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L926) Ref - The user-facing ref interface for the query builder diff --git a/docs/reference/type-aliases/RefsForContext.md b/docs/reference/type-aliases/RefsForContext.md index c7e9057ec9..fa220dd355 100644 --- a/docs/reference/type-aliases/RefsForContext.md +++ b/docs/reference/type-aliases/RefsForContext.md @@ -7,7 +7,7 @@ title: RefsForContext type RefsForContext = { [K in Exclude>, keyof JoinedRefsForContext | keyof BranchUnionResultRefs>]: RefForContextSchemaValue, K>, IsNullableContextKey> } & TContext["hasResult"] extends true ? object : object & BranchUnionResultRefs & JoinedRefsForContext; ``` -Defined in: [packages/db/src/query/builder/types.ts:762](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L762) +Defined in: [packages/db/src/query/builder/types.ts:788](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L788) ## Type Parameters diff --git a/docs/reference/type-aliases/ResultTypeFromSelect.md b/docs/reference/type-aliases/ResultTypeFromSelect.md index f52d8d7c8f..afc4261124 100644 --- a/docs/reference/type-aliases/ResultTypeFromSelect.md +++ b/docs/reference/type-aliases/ResultTypeFromSelect.md @@ -7,7 +7,7 @@ title: ResultTypeFromSelect type ResultTypeFromSelect = IsAny extends true ? any : IsTrueRef extends true ? ExtractDirectSelectRef : WithoutRefBrand extends true ? ExtractExpressionType : TSelectObject[K] extends ToArrayWrapper ? T[] : TSelectObject[K] extends ConcatToArrayWrapper ? string : TSelectObject[K] extends MaterializeWrapper ? IsSingle extends true ? (...) | (...) : (...)[] : (...)[(...)] extends { __brand: ...; _result?: ... } ? ResultTypeFromCaseWhen<(...)> : (...) extends (...) ? (...) : (...) }>>; ``` -Defined in: [packages/db/src/query/builder/types.ts:414](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L414) +Defined in: [packages/db/src/query/builder/types.ts:440](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L440) ResultTypeFromSelect - Infers the result type from a select object diff --git a/docs/reference/type-aliases/WithResult.md b/docs/reference/type-aliases/WithResult.md index 6b8d2ead42..359e52e864 100644 --- a/docs/reference/type-aliases/WithResult.md +++ b/docs/reference/type-aliases/WithResult.md @@ -7,7 +7,7 @@ title: WithResult type WithResult = Prettify & Pick & object>; ``` -Defined in: [packages/db/src/query/builder/types.ts:1360](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1360) +Defined in: [packages/db/src/query/builder/types.ts:1386](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1386) WithResult - Updates a context with a new result type after select() diff --git a/docs/reference/variables/Query.md b/docs/reference/variables/Query.md index dea94b2935..85d2435c1f 100644 --- a/docs/reference/variables/Query.md +++ b/docs/reference/variables/Query.md @@ -7,4 +7,4 @@ title: Query const Query: InitialQueryBuilderConstructor = BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:1785](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1785) +Defined in: [packages/db/src/query/builder/index.ts:1788](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1788) diff --git a/docs/reference/variables/fullJoin.md b/docs/reference/variables/fullJoin.md index dea904f2d5..19996ab3b7 100644 --- a/docs/reference/variables/fullJoin.md +++ b/docs/reference/variables/fullJoin.md @@ -7,4 +7,4 @@ title: fullJoin const fullJoin: JoinOperatorFunction<"full">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:60](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L60) +Defined in: [packages/db/src/query/builder/join-operators.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L72) diff --git a/docs/reference/variables/innerJoin.md b/docs/reference/variables/innerJoin.md index 07f435f350..3d113df267 100644 --- a/docs/reference/variables/innerJoin.md +++ b/docs/reference/variables/innerJoin.md @@ -7,4 +7,4 @@ title: innerJoin const innerJoin: JoinOperatorFunction<"inner">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L59) +Defined in: [packages/db/src/query/builder/join-operators.ts:71](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L71) diff --git a/docs/reference/variables/leftJoin.md b/docs/reference/variables/leftJoin.md index adc7092f16..ec2b6aebfd 100644 --- a/docs/reference/variables/leftJoin.md +++ b/docs/reference/variables/leftJoin.md @@ -7,4 +7,4 @@ title: leftJoin const leftJoin: JoinOperatorFunction<"left">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:57](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L57) +Defined in: [packages/db/src/query/builder/join-operators.ts:69](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L69) diff --git a/docs/reference/variables/operators.md b/docs/reference/variables/operators.md index 8ecb781d35..ebe26fa5b7 100644 --- a/docs/reference/variables/operators.md +++ b/docs/reference/variables/operators.md @@ -7,6 +7,6 @@ title: operators const operators: readonly ["eq", "gt", "gte", "lt", "lte", "in", "like", "ilike", "and", "or", "not", "isNull", "isUndefined", "upper", "lower", "length", "concat", "add", "subtract", "multiply", "divide", "coalesce", "caseWhen", "count", "avg", "sum", "min", "max"]; ``` -Defined in: [packages/db/src/query/builder/functions.ts:722](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L722) +Defined in: [packages/db/src/query/builder/functions.ts:724](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L724) All supported operator names in TanStack DB expressions diff --git a/docs/reference/variables/rightJoin.md b/docs/reference/variables/rightJoin.md index c7698c7dde..6d31a927b2 100644 --- a/docs/reference/variables/rightJoin.md +++ b/docs/reference/variables/rightJoin.md @@ -7,4 +7,4 @@ title: rightJoin const rightJoin: JoinOperatorFunction<"right">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L58) +Defined in: [packages/db/src/query/builder/join-operators.ts:70](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L70) diff --git a/package.json b/package.json index 6729e5169d..fe59f2e9dc 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "size:compare": "node scripts/size-compare.mjs", "typecheck:tests": "pnpm --filter @tanstack/db-ivm --filter @tanstack/offline-transactions typecheck", "test:docs": "node scripts/verify-links.ts", + "test:doc-snippets": "node scripts/check-doc-snippets.mjs", "test:sherif": "sherif -i zod -p offline-transactions-react-native -p shopping-list-react-native", "generate-docs": "node scripts/generate-docs.ts" }, diff --git a/packages/db/skills/db-core/live-queries/SKILL.md b/packages/db/skills/db-core/live-queries/SKILL.md index 1b71f41900..b0bd0feb90 100644 --- a/packages/db/skills/db-core/live-queries/SKILL.md +++ b/packages/db/skills/db-core/live-queries/SKILL.md @@ -118,13 +118,16 @@ non-null value. Join conditions accept `eq()` or a nonempty, possibly nested `and()` of equalities (equality only -- IVM constraint). Joins take an imported operator: `.join(leftJoin(source, on))`, and likewise `innerJoin`, `rightJoin`, `fullJoin`, and `joinOf(type, source, on)`. The `.join(source, on)` call shape and the `.leftJoin()` methods are removed and throw `JoinOperatorRequiredError`. ```ts -import { eq, innerJoin } from '@tanstack/db' +import { createLiveQueryCollection, eq, innerJoin } from '@tanstack/db' const userPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(innerJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + innerJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, @@ -136,9 +139,16 @@ Multiple joins: ```ts q.from({ user: usersCollection }) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId))) - .join(leftJoin({ comment: commentsCollection }, ({ post, comment }) => - eq(post.id, comment.postId))) + .join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) + .join( + leftJoin({ comment: commentsCollection }, ({ post, comment }) => + eq(post.id, comment.postId), + ), + ) ``` ### 3. Aggregation with groupBy + having @@ -188,8 +198,11 @@ const activeUsers = createLiveQueryCollection((q) => const activeUserPosts = createLiveQueryCollection((q) => q .from({ user: activeUsers }) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId))) + .join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), + ) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, @@ -243,18 +256,20 @@ const projectsWithIssues = createLiveQueryCollection((q) => q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, - issues: include(q - .from({ i: issuesCollection }) - .where(({ i }) => eq(i.projectId, p.id)) - .select(({ i }) => ({ - id: i.id, - title: i.title, - }))), + issues: include( + q + .from({ i: issuesCollection }) + .where(({ i }) => eq(i.projectId, p.id)) + .select(({ i }) => ({ + id: i.id, + title: i.title, + })), + ), })), ) // Each row's `issues` is a live Collection -for (const project of projectsWithIssues) { +for (const project of projectsWithIssues.toArray) { console.log(project.name, project.issues.toArray) } ``` @@ -529,9 +544,11 @@ q.from({ order: ordersCollection }) ```ts import { and, eq, leftJoin } from '@tanstack/db' -q.from({ product: productsCollection }).join(leftJoin({ stock: stockCollection }, - ({ product, stock }) => - and(eq(product.sku, stock.sku), eq(product.region, stock.region)))) +q.from({ product: productsCollection }).join( + leftJoin({ stock: stockCollection }, ({ product, stock }) => + and(eq(product.sku, stock.sku), eq(product.region, stock.region)), + ), +) ``` All equalities must match. A nullish component never matches, and outer joins @@ -563,12 +580,18 @@ The differential dataflow join operator supports a single equality or nested `an ```ts // WRONG -q.from({ user: usersCollection }).join(leftJoin({ post: postsCollection }, - ({ user, post }) => gt(user.id, post.userId))) +q.from({ user: usersCollection }).join( + leftJoin({ post: postsCollection }, ({ user, post }) => + gt(user.id, post.userId), + ), +) // CORRECT -q.from({ user: usersCollection }).join(leftJoin({ post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId))) +q.from({ user: usersCollection }).join( + leftJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), + ), +) ``` ### MEDIUM: Passing source directly instead of {alias: collection} diff --git a/packages/db/src/query/builder/functions.ts b/packages/db/src/query/builder/functions.ts index b2f1e59b69..ab24ebc89e 100644 --- a/packages/db/src/query/builder/functions.ts +++ b/packages/db/src/query/builder/functions.ts @@ -396,9 +396,11 @@ export function coalesce]>( * ```ts * caseWhen(gt(user.age, 18), { * ...user, - * posts: q - * .from({ post: postsCollection }) - * .where(({ post }) => eq(post.userId, user.id)), + * posts: include( + * q + * .from({ post: postsCollection }) + * .where(({ post }) => eq(post.userId, user.id)), + * ), * }) * ``` */ diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index dc1391c654..11a3c4ef71 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -84,6 +84,7 @@ import type { Source, WhereCallback, WithResult, + WithoutBareSubquery, } from './types.js' const UNION_ALL_SOURCE_CONTEXT = `unionAll clause` satisfies SourceClauseContext @@ -547,7 +548,9 @@ export class BaseQueryBuilder { ) => NonScalarSelectObject, ): QueryBuilder>> select( - callback: (refs: RefsForContext) => TSelectValue, + callback: ( + refs: RefsForContext, + ) => WithoutBareSubquery, ): QueryBuilder>> select( callback: ( diff --git a/packages/db/src/query/builder/types.ts b/packages/db/src/query/builder/types.ts index e280b14c34..b5616539e0 100644 --- a/packages/db/src/query/builder/types.ts +++ b/packages/db/src/query/builder/types.ts @@ -314,10 +314,36 @@ type IsScalarSelectLike = T extends BasicExpression | Aggregate ? false : true : false +// A select value must not hold a bare subquery: it needs include(), toArray(), +// or materialize(). The open ref and user-value shapes cannot express this, so +// both select overloads check it. The check stops at wrappers and expressions, +// which hold builders of their own, and a few levels deep. +type ContainsQueryBuilder< + T, + TDepth extends Array = [], +> = TDepth[`length`] extends 4 + ? false + : IsAny extends true + ? false + : T extends QueryBuilder + ? true + : T extends BasicExpression | Aggregate | { readonly __brand: string } + ? false + : T extends object + ? true extends { + [K in keyof T]-?: ContainsQueryBuilder + }[keyof T] + ? true + : false + : false + +export type WithoutBareSubquery = + ContainsQueryBuilder extends true ? never : T + export type NonScalarSelectObject = T extends SelectObject ? IsScalarSelectLike extends true ? never - : T + : WithoutBareSubquery : never export type ResultTypeFromSelectValue = diff --git a/packages/db/tests/join-typescript-modes.test.ts b/packages/db/tests/join-typescript-modes.test.ts index 8ff91e1295..251bdbd470 100644 --- a/packages/db/tests/join-typescript-modes.test.ts +++ b/packages/db/tests/join-typescript-modes.test.ts @@ -8,28 +8,63 @@ import { describe, expect, it } from 'vitest' // must still compile there, untyped like other callbacks in that mode. // Strict mode must keep it typed. Type tests run in strict mode only, so this // compiles one fixture in both modes. -const fixture = fileURLToPath( - new URL(`./fixtures/loose-typescript-joins.ts`, import.meta.url), -) +const fixture = (name: string) => + fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)) const config = ts.getParsedCommandLineOfConfigFile( fileURLToPath(new URL(`../tsconfig.json`, import.meta.url)), {}, { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, )! -function errors(noImplicitAny: boolean) { - const program = ts.createProgram([fixture], { - ...config.options, - noImplicitAny, - noEmit: true, - }) +// A source with a deliberate error lives in memory, so the project's own +// typecheck does not see it. +const wrongField = fixture(`strict-join-field-error.ts`) +const wrongFieldSource = `// Compiled by join-typescript-modes.test.ts. Strict mode must reject the +// wrong field; without noImplicitAny the condition is untyped. +import { createLiveQueryCollection, eq, leftJoin } from '../../src/index.js' +import type { Collection } from '../../src/index.js' + +type User = { id: number; name: string } +type Post = { id: number; userId: number } +declare const users: Collection +declare const posts: Collection + +export const joined = createLiveQueryCollection((q) => + q + .from({ user: users }) + .join( + leftJoin({ post: posts }, ({ user, post }) => eq(user.nope, post.userId)), + ), +) +` + +function errors(file: string, noImplicitAny: boolean) { + const options = { ...config.options, noImplicitAny, noEmit: true } + const host = ts.createCompilerHost(options) + const readFile = host.readFile + host.readFile = (path) => + path === wrongField ? wrongFieldSource : readFile(path) + const fileExists = host.fileExists + host.fileExists = (path) => path === wrongField || fileExists(path) + const program = ts.createProgram([file], options, host) return ts - .getPreEmitDiagnostics(program, program.getSourceFile(fixture)) + .getPreEmitDiagnostics(program, program.getSourceFile(file)) .map((d) => ts.flattenDiagnosticMessageText(d.messageText, ` `)) } describe(`join conditions across TypeScript modes`, () => { it.each([true, false])(`compile with noImplicitAny %s`, (noImplicitAny) => { - expect(errors(noImplicitAny)).toEqual([]) + expect(errors(fixture(`loose-typescript-joins.ts`), noImplicitAny)).toEqual( + [], + ) + }) + + // The untyped fallback must not reach strict mode: a wrong field still fails. + it(`keep a wrong field an error in strict mode only`, () => { + const file = wrongField + expect(errors(file, true)).toEqual([ + expect.stringContaining(`Property 'nope' does not exist`), + ]) + expect(errors(file, false)).toEqual([]) }) }) diff --git a/packages/db/tests/query/includes.test-d.ts b/packages/db/tests/query/includes.test-d.ts index 7f0aadbfe6..d24c9d3b12 100644 --- a/packages/db/tests/query/includes.test-d.ts +++ b/packages/db/tests/query/includes.test-d.ts @@ -557,3 +557,24 @@ describe(`includes subquery types`, () => { }) }) }) + +describe(`a subquery in select needs an include function`, () => { + test(`rejects a bare subquery field, also when nested`, () => { + const projects = createProjectsCollection() + const issues = createIssuesCollection() + createLiveQueryCollection((q) => + // @ts-expect-error A bare subquery field needs include(). + q.from({ p: projects }).select(({ p }) => ({ + issues: q.from({ i: issues }).where(({ i }) => eq(i.projectId, p.id)), + })), + ) + createLiveQueryCollection((q) => + // @ts-expect-error A nested bare subquery field needs include(). + q.from({ p: projects }).select(({ p }) => ({ + detail: { + issues: q.from({ i: issues }).where(({ i }) => eq(i.projectId, p.id)), + }, + })), + ) + }) +}) diff --git a/scripts/check-doc-snippets.mjs b/scripts/check-doc-snippets.mjs new file mode 100644 index 0000000000..e0320bdc61 --- /dev/null +++ b/scripts/check-doc-snippets.mjs @@ -0,0 +1,334 @@ +// Typechecks the TypeScript code examples in the guides and the shipped agent +// skills against the workspace packages' source. +// +// Examples are fragments: they use collections and components they never +// declare. The check declares each unknown name as `any`, so it reports what +// an example gets wrong about the TanStack API rather than what it leaves +// out. An unknown name that a TanStack package exports is reported instead: +// the example uses it without importing it. +// +// node scripts/check-doc-snippets.mjs [--verbose] [--update-baseline] +// +// Mark a fence ```ts nocheck to skip an example that is not meant to compile. +// +// `doc-snippets-baseline.json` lists examples that already failed when the +// check began, by file and a hash of their code. Editing one changes its +// hash, so an edited example must pass. An example that passes must leave the +// baseline: run with --update-baseline after fixing one. +// +// Debugging: SNIPPET_ONLY=: checks one example, and +// SNIPPET_DUMP=: prints the source the check compiles for it. +import { createHash } from 'node:crypto' +import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' +import { join, relative } from 'node:path' +import { fileURLToPath } from 'node:url' +import prettier from 'prettier' +import ts from 'typescript' + +const root = fileURLToPath(new URL(`..`, import.meta.url)) +const verbose = process.argv.includes(`--verbose`) +const updateBaseline = process.argv.includes(`--update-baseline`) +const baselinePath = join(root, `scripts`, `doc-snippets-baseline.json`) +const baseline = existsSync(baselinePath) + ? JSON.parse(readFileSync(baselinePath, `utf8`)) + : {} +const hashOf = (code) => + createHash(`sha256`).update(code.trim()).digest(`hex`).slice(0, 16) + +function walk(dir, keep) { + if (!existsSync(dir)) return [] + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const path = join(dir, entry.name) + if (entry.isDirectory()) + return entry.name === `node_modules` || entry.name === `reference` + ? [] + : walk(path, keep) + return keep(path) ? [path] : [] + }) +} + +const markdown = [ + ...walk(join(root, `docs`), (path) => path.endsWith(`.md`)), + ...readdirSync(join(root, `packages`)).flatMap((pkg) => + walk(join(root, `packages`, pkg, `skills`), (path) => path.endsWith(`.md`)), + ), +] + +// Map each workspace package to its source entry. +const paths = {} +for (const pkg of readdirSync(join(root, `packages`))) { + const manifest = join(root, `packages`, pkg, `package.json`) + if (!existsSync(manifest)) continue + const { name, private: unpublished } = JSON.parse( + readFileSync(manifest, `utf8`), + ) + // Examples document published packages only. + if (unpublished) continue + for (const entry of [`src/index.ts`, `src/index.tsx`]) { + if (existsSync(join(root, `packages`, pkg, entry))) { + paths[name] = [join(`packages`, pkg, entry)] + break + } + } +} + +const fence = /```(ts|tsx|typescript)([^\n]*)\n([\s\S]*?)```/g +const snippets = [] +for (const file of markdown) { + const text = readFileSync(file, `utf8`) + for (const match of text.matchAll(fence)) { + if (match[2].includes(`nocheck`)) continue + const line = text.slice(0, match.index).split(`\n`).length + snippets.push({ + file: relative(root, file), + line, + code: match[3], + name: join( + root, + `.snippet-check`, + `${snippets.length}.${/<\/|<[A-Z][A-Za-z]*[ />]/.test(match[3]) ? `tsx` : `ts`}`, + ), + }) + } +} + +if (process.env.SNIPPET_ONLY) { + const keep = snippets.filter( + (s) => `${s.file}:${s.line}` === process.env.SNIPPET_ONLY, + ) + snippets.length = 0 + snippets.push(...keep) +} + +const options = { + strict: true, + noUnusedLocals: false, + noUncheckedIndexedAccess: false, + allowUnreachableCode: true, + skipLibCheck: true, + noEmit: true, + jsx: ts.JsxEmit.ReactJSX, + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + lib: [`lib.es2022.d.ts`, `lib.dom.d.ts`, `lib.dom.iterable.d.ts`], + baseUrl: root, + paths, +} + +// Modules an example imports but this repo cannot resolve (app files, other +// frameworks) are declared `any`, one by one. A wildcard would also erase +// third-party types that the packages themselves use. +const ambient = join(root, `.snippet-check`, `ambient.d.ts`) +const missingModules = new Set() +const ambientSource = () => + [...missingModules] + .map((name) => `declare module ${JSON.stringify(name)}\n`) + .join(``) || `export {}\n` + +function check(sources) { + const host = ts.createCompilerHost(options) + const read = host.readFile + host.readFile = (path) => sources.get(path) ?? read(path) + const exists = host.fileExists + host.fileExists = (path) => sources.has(path) || exists(path) + const program = ts.createProgram([...sources.keys()], options, host) + return { program, diagnostics: ts.getPreEmitDiagnostics(program) } +} + +// Names the TanStack packages export, to tell "not imported" from "app code". +const exported = new Map() +{ + const probe = join(root, `.snippet-check`, `exports.ts`) + const packages = Object.keys(paths).filter((name) => + name.startsWith(`@tanstack/`), + ) + const imports = packages + .map((name, i) => `import * as m${i} from '${name}'`) + .join(`\n`) + const { program } = check(new Map([[probe, imports + `\n`]])) + const checker = program.getTypeChecker() + program.getSourceFile(probe).statements.forEach((statement, i) => { + const symbol = checker.getSymbolAtLocation(statement.moduleSpecifier) + if (!symbol) return + for (const item of checker.getExportsOfModule(symbol)) { + const owners = exported.get(item.name) ?? [] + owners.push(packages[i]) + exported.set(item.name, owners) + } + }) +} + +// The framework a file documents, for names several adapters export. +const frameworkOf = (file) => + [`angular`, `solid`, `svelte`, `vue`, `react`].find((name) => + file.includes(name), + ) ?? `react` + +function packageFor(name, file) { + const owners = exported.get(name) + if (owners.length === 1) return owners[0] + return ( + owners.find((owner) => owner === `@tanstack/db`) ?? + owners.find((owner) => owner.includes(frameworkOf(file))) ?? + owners[0] + ) +} + +const importsTanStack = (code) => /from ['"]@tanstack\//.test(code) + +function sourceOf(snippet, declared, autoImported) { + const byPackage = new Map() + for (const name of autoImported) { + const pkg = packageFor(name, snippet.file) + byPackage.set(pkg, [...(byPackage.get(pkg) ?? []), name]) + } + return ( + [...byPackage] + .map(([pkg, names]) => `import { ${names.join(`, `)} } from '${pkg}'\n`) + .join(``) + + `import type { Collection as SnippetCollection } from '@tanstack/db'\n` + + [...declared] + .map( + (name) => + `declare const ${name}: ${/collection$/i.test(name) ? `SnippetCollection, string | number>` : `any`}\ntype ${name} = any\n`, + ) + .join(``) + + snippet.code + + `\nexport {}\n` + ) +} + +const declared = snippets.map(() => new Set()) +const unimported = snippets.map(() => new Set()) +const autoImported = snippets.map(() => new Set()) +const unknownName = new Set([2304, 2552, 18004, 2749, 2693]) +// Docs often show alternatives side by side in one block, and leave +// callback parameters unannotated where an app would annotate them. +const ignored = new Set([2451, 2300, 2393, 7005, 7006, 7019, 7031, 7034, 7053]) +// An unresolvable dependency is declared as an untyped module, whose names +// cannot be used as types. +ignored.add(2709) +let diagnostics = [] +// Declare unknown names until a pass adds none. +for (let pass = 0; pass < 6; pass++) { + const sources = new Map([[ambient, ambientSource()]]) + snippets.forEach((snippet, i) => + sources.set(snippet.name, sourceOf(snippet, declared[i], autoImported[i])), + ) + diagnostics = check(sources).diagnostics + let added = 0 + for (const diagnostic of diagnostics) { + if ( + diagnostic.code === 2307 && + snippets.some((s) => s.name === diagnostic.file?.fileName) + ) { + const name = ts + .flattenDiagnosticMessageText(diagnostic.messageText, ``) + .match(/'([^']+)'/)?.[1] + if (name && !missingModules.has(name)) { + missingModules.add(name) + added++ + } + continue + } + if (!unknownName.has(diagnostic.code)) continue + const index = snippets.findIndex( + (s) => s.name === diagnostic.file?.fileName, + ) + if (index < 0) continue + const name = ts + .flattenDiagnosticMessageText(diagnostic.messageText, ``) + .match(/'([^']+)'/)?.[1] + if (!name) continue + if (exported.has(name)) { + if (importsTanStack(snippets[index].code)) unimported[index].add(name) + else if (!autoImported[index].has(name)) { + autoImported[index].add(name) + added++ + } + } else if (!declared[index].has(name)) { + declared[index].add(name) + added++ + } + } + if (added === 0) break +} + +if (process.env.SNIPPET_DUMP) { + const i = snippets.findIndex( + (s) => `${s.file}:${s.line}` === process.env.SNIPPET_DUMP, + ) + console.log(sourceOf(snippets[i], declared[i], autoImported[i])) +} +const failures = [] +let unparsable = 0 +snippets.forEach((snippet, i) => { + const ownDiagnostics = diagnostics.filter( + (d) => d.file?.fileName === snippet.name, + ) + // Pseudo-code, such as `...` placeholders, is not meant to compile. + if (ownDiagnostics.some((d) => d.code >= 1000 && d.code < 2000)) { + unparsable++ + return + } + const offset = + 1 + + declared[i].size * 2 + + new Set([...autoImported[i]].map((n) => packageFor(n, snippet.file))).size + const messages = ownDiagnostics + .filter((d) => !unknownName.has(d.code) && !ignored.has(d.code)) + .map((d) => { + const { line } = d.file.getLineAndCharacterOfPosition(d.start ?? 0) + return ` line ${snippet.line + line - offset}: TS${d.code} ${ts.flattenDiagnosticMessageText(d.messageText, ` `)}` + }) + for (const name of unimported[i]) + messages.push(` uses \`${name}\` without importing it`) + if (messages.length) + failures.push({ + snippet, + text: `${snippet.file}:${snippet.line}\n${messages.join(`\n`)}`, + }) +}) + +const known = (snippet) => + baseline[snippet.file]?.includes(hashOf(snippet.code)) ?? false +const newFailures = failures.filter(({ snippet }) => !known(snippet)) +const failing = new Set( + failures.map(({ snippet }) => `${snippet.file}#${hashOf(snippet.code)}`), +) +const stale = Object.entries(baseline).flatMap(([file, hashes]) => + hashes + .filter((hash) => !failing.has(`${file}#${hash}`)) + .map((hash) => `${file}#${hash}`), +) + +if (updateBaseline && !process.env.SNIPPET_ONLY) { + const next = {} + for (const { snippet } of failures) + (next[snippet.file] ??= []).push(hashOf(snippet.code)) + for (const hashes of Object.values(next)) hashes.sort() + const sorted = Object.fromEntries( + Object.entries(next).sort(([a], [b]) => a.localeCompare(b)), + ) + writeFileSync( + baselinePath, + await prettier.format(JSON.stringify(sorted), { + ...(await prettier.resolveConfig(baselinePath)), + filepath: baselinePath, + }), + ) + console.log(`Baseline: ${failures.length} known failing examples`) +} else { + const shown = verbose ? failures : newFailures + if (shown.length) console.log(shown.map(({ text }) => text).join(`\n\n`)) + if (stale.length && !process.env.SNIPPET_ONLY) + console.log( + `\n${stale.length} baseline entries now pass or no longer exist; run with --update-baseline:\n${stale.join(`\n`)}`, + ) + console.log( + `\n${snippets.length} examples in ${markdown.length} files: ${newFailures.length} new failures, ${failures.length - newFailures.length} known, ${unparsable} skipped as pseudo-code`, + ) + process.exitCode = + newFailures.length || (stale.length && !process.env.SNIPPET_ONLY) ? 1 : 0 +} diff --git a/scripts/doc-snippets-baseline.json b/scripts/doc-snippets-baseline.json new file mode 100644 index 0000000000..6cf6a5be54 --- /dev/null +++ b/scripts/doc-snippets-baseline.json @@ -0,0 +1,273 @@ +{ + "docs/collections/electric-collection.md": [ + "0dd50060adbf8c3f", + "102e3b6860d47f70", + "54275ea6e4b24910" + ], + "docs/collections/indexed-db-collection.md": ["dad4e77c4763f32a"], + "docs/collections/local-only-collection.md": [ + "35d97a3a9d141256", + "3615b30b88224e39", + "36f0f65995ced962", + "62bfffbdd760af64", + "78e9144a8c4d6946" + ], + "docs/collections/local-storage-collection.md": [ + "22017ba94407fe7d", + "2add0a25c7533df9", + "3663c960d1461dde", + "7a5ee2385c57e6da", + "93529b815b56005f", + "952d65ad77259ef8", + "a23b02c8ebff50ea" + ], + "docs/collections/powersync-collection.md": [ + "1afab707dfed2b14", + "20090ec993fa884d", + "4dd21379bff94d51", + "54e13a571814f441", + "b7a80e0fdee7efdc" + ], + "docs/collections/query-collection.md": [ + "0c7c57c2f2daf128", + "25caab8b5e0160ca", + "40db37e1b3b1649f", + "46c646552bb94fa1", + "46eafbc648a2de41", + "4936e66d71ebc0b5", + "79702ff5b213cb74", + "7ec88a76700c9ef5", + "8f3f399ac425424a", + "947554ae2af1c47d", + "9d6166c311d0d6e6", + "a3687d66ef982466", + "a5b52c9f8af29d00", + "e14f245e9749d168", + "ea2ff4dd09fbb02f" + ], + "docs/collections/query-initial-placeholder-data-design.md": [ + "9edcfc23f56a849b" + ], + "docs/collections/trailbase-collection.md": [ + "01bc2c23f090ee82", + "8e6131fa04843059", + "c5e5da6b0528d05a", + "da0fb4a61a4b1258", + "e5fd5fd7cf3ee7af" + ], + "docs/contributing/oracle-reviews/initial-full-source-sync-readiness.md": [ + "db1f0953a754e669" + ], + "docs/framework/angular/overview.md": ["d39903c5982ee517"], + "docs/framework/react/overview.md": ["74b26b1d16180ce0", "b0e6d12ea9882897"], + "docs/framework/solid/overview.md": ["505fbc1660154c2b"], + "docs/guides/collection-options-creator.md": [ + "26cf7b70bf5a9328", + "6555958fbcbbe3f3", + "6b31a02781cc0b6f", + "7bec8c32bbcdd756", + "7ef713bfd5fc90d4", + "8bad2af899e2098a", + "a04db2d01d988b05", + "ada9c5856d6af91e", + "e31e1498002eef0b" + ], + "docs/guides/error-handling.md": [ + "11d18030135ddad1", + "2562d7c6eeede61b", + "2dce561a1a49b614", + "339019147dd88b41", + "392bbc3bc9e5790c", + "3a3ba04c9ccdeb61", + "3f29a3e83096bccb", + "73bf36e975556612", + "82a5eb0bd5e837b4", + "adaec9d8439a0d76", + "c2db832bb6875e28", + "c740378204672b1c", + "d92784296ae4c98a", + "e647ec0d24c013f6", + "f5a0e5835193f709" + ], + "docs/guides/live-queries.md": [ + "01f663e8b4b7a5c5", + "03b16ea37af5155a", + "193a6540c5cb9751", + "3168fcfa7b402bd5", + "3c7650d829b70c58", + "41ad2967e4792981", + "44a43649d5328fee", + "5049db06a1bca6c2", + "54b2845b3f4a3a94", + "5fcce92ba0507289", + "70bbc60053a33fad", + "a4cbec996c7c4cb5", + "bdc9c30c57add7cd", + "bfaf11792701b5a6", + "cdddcc4e9dff0a9b", + "d42c540f286dedc0", + "d5628fff0679e5dc", + "f113d47460e2166b", + "f4ba391e288f96cc" + ], + "docs/guides/mutations.md": [ + "08261373bf652f48", + "1255521980bf984d", + "1397fc8a87edfa8b", + "1b9ddb3f971078cc", + "2e74aee8c03a2c23", + "3ed04a2948b0b6e0", + "479ded16124a4b79", + "6ab013c30a539cb0", + "6e5d7d2bc0e7944a", + "779413de3b4085a2", + "7e5e18900577074f", + "839efb04d2715746", + "8c968ac8b3f2b84f", + "8d38c4925d5a1918", + "ad561652b2f92f13", + "be1a2f6b93290baa", + "c3e85f5abfc6333d", + "cecaaeb48d5e7a8b", + "e48a63a8418dd5f6", + "fffbd38b18827d0e" + ], + "docs/guides/offline-transactions.md": [ + "24e0163dcb007fab", + "8d210fead79f3cf0" + ], + "docs/guides/schemas.md": [ + "136aaed9cbe5ea61", + "19b138ed11f82f63", + "21c20e07e4bbc178", + "2320661c518a4ad3", + "2f838bf85e0da434", + "492dbb0212c677de", + "7019266cd52d2ed5", + "877b3a9c16c15da2", + "9ceb0ac6e0564630", + "aadec84ec61f60f1", + "dbf1970a78767f17", + "ebed33ff5f590c48", + "f00d518c1dc0f352" + ], + "docs/guides/sqlite-persistence.md": [ + "0bc141ca7a9cecaa", + "76993422b4c8881f", + "be59956fa556b952", + "dcf611fc0de7a5ea" + ], + "docs/guides/ssr.md": [ + "2eeb8be63193e90e", + "3f521af644cc4a6c", + "7df90009f17bb64f", + "bae874816b80fa76", + "d0c5531a14522216" + ], + "docs/overview.md": [ + "17addcbdcdc8bd9d", + "25834862ad19e87b", + "49c4d17ae089392e", + "6b316852f49bd7e1", + "7f2e477746fb333c", + "b218fcb90eae1b25" + ], + "docs/quick-start.md": [ + "031935aa43874a0a", + "3dc699a8394a7d6d", + "73d5900ac8e2d76b", + "74b26b1d16180ce0" + ], + "packages/angular-db/skills/angular-db/SKILL.md": [ + "0039ec2323554d77", + "1c2d766183b1c4ed", + "2170cb84aea1c46e", + "2e9b57d202697885", + "373e31c9185d8b2d" + ], + "packages/db/skills/db-core/collection-setup/references/local-adapters.md": [ + "131410fc478710cc", + "1a15d24213289e11", + "ba6f6ce87766c12f", + "fb57b98d2e7f574a" + ], + "packages/db/skills/db-core/collection-setup/references/powersync-adapter.md": [ + "9667df44886f0ca2" + ], + "packages/db/skills/db-core/collection-setup/references/query-adapter.md": [ + "458f58547abe2d59", + "4f6f60340f2e2c0b", + "8a1c6eada500cff8", + "9b0bf4836d0cfb78", + "b6712a06f76aabeb", + "c034a05d65b63d4a", + "d1afd54756a54a67" + ], + "packages/db/skills/db-core/collection-setup/references/trailbase-adapter.md": [ + "049d5225d8e5e24a" + ], + "packages/db/skills/db-core/collection-setup/SKILL.md": [ + "0ce632547d2f3429", + "1860f97d7af4d664", + "2045994875b4948a", + "4bdb69d627811855", + "9760d69c1a43223c", + "b6846dcda912e886", + "c529358f3d493b5a", + "cc984debd9673623", + "ebca8a1eea44c425", + "ef0695b2edac1d47", + "f807227957b45b54" + ], + "packages/db/skills/db-core/custom-adapter/SKILL.md": ["b3c120c7a08c2366"], + "packages/db/skills/db-core/live-queries/references/operators.md": [ + "4b17b68395b1fe83" + ], + "packages/db/skills/db-core/live-queries/SKILL.md": [ + "32a783108e6f18bf", + "738472679137647d", + "918167034910a1e9", + "b1246c8734027a6a" + ], + "packages/db/skills/db-core/mutations-optimistic/references/transaction-api.md": [ + "e35c8663478b601a" + ], + "packages/db/skills/db-core/mutations-optimistic/SKILL.md": [ + "1337a5a5bcd052bd", + "4f3879defa7fb44f", + "aebe19060ddcbd7c", + "d289f1c191a383e3", + "e720a019114309a8" + ], + "packages/db/skills/db-core/persistence/SKILL.md": [ + "132b43a15b4c1d96", + "65b4f94b03be5df4", + "6a97114189308f12", + "8e483f20ce54a2cd", + "ba99444cf808cdc1", + "e17e1d3eedb30ae4" + ], + "packages/db/skills/meta-framework/SKILL.md": ["77fd28263fca116b"], + "packages/offline-transactions/skills/offline/SKILL.md": [ + "120091041b09393e", + "1a506e324de141a4", + "4bb0f7db77cc9451", + "dbd64ce5b54e79cf" + ], + "packages/react-db/skills/react-db/SKILL.md": [ + "23255f3913c6cc54", + "5828824fffb42015", + "6cea34f4dcc65cae", + "e18a2c5d10ddc044" + ], + "packages/solid-db/skills/solid-db/SKILL.md": ["fc051f29c5d4f27e"], + "packages/svelte-db/skills/svelte-db/SKILL.md": [ + "502c3a8c40af4e0d", + "56cbaae9c63c2fc2", + "ed4d80558901d985" + ], + "packages/vue-db/skills/vue-db/SKILL.md": [ + "4cbe8baab108bd25", + "8bcb70ab4bdfacfc" + ] +} From 32d3f344ee1e46be56c105ba31832e6120b0bf69 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 23 Oct 2026 10:03:16 -0600 Subject: [PATCH 18/32] test: key the doc example baseline on tokens, not formatting The autofix bot reformatted skill examples, which changed their hashes and made known failures look new. The baseline now hashes each example's tokens without commas, semicolons, parentheses, and spacing, so a reformatted example keeps its entry. An edit to its code still does not. Co-authored-by: Isaac --- scripts/check-doc-snippets.mjs | 38 ++- scripts/doc-snippets-baseline.json | 414 ++++++++++++++--------------- 2 files changed, 240 insertions(+), 212 deletions(-) diff --git a/scripts/check-doc-snippets.mjs b/scripts/check-doc-snippets.mjs index e0320bdc61..90dd7e432e 100644 --- a/scripts/check-doc-snippets.mjs +++ b/scripts/check-doc-snippets.mjs @@ -13,7 +13,7 @@ // // `doc-snippets-baseline.json` lists examples that already failed when the // check began, by file and a hash of their code. Editing one changes its -// hash, so an edited example must pass. An example that passes must leave the +// hash, so an edited example must pass; reformatting one keeps it. An example that passes must leave the // baseline: run with --update-baseline after fixing one. // // Debugging: SNIPPET_ONLY=: checks one example, and @@ -32,8 +32,34 @@ const baselinePath = join(root, `scripts`, `doc-snippets-baseline.json`) const baseline = existsSync(baselinePath) ? JSON.parse(readFileSync(baselinePath, `utf8`)) : {} +// The tokens of an example, without the punctuation and spacing that +// formatting changes, so a reformatted example keeps its baseline entry. +function fingerprint(code) { + const scanner = ts.createScanner(ts.ScriptTarget.Latest, true) + scanner.setText(code) + const skip = new Set([ + ts.SyntaxKind.CommaToken, + ts.SyntaxKind.SemicolonToken, + ts.SyntaxKind.OpenParenToken, + ts.SyntaxKind.CloseParenToken, + ]) + const tokens = [] + for ( + let kind = scanner.scan(); + kind !== ts.SyntaxKind.EndOfFileToken; + kind = scanner.scan() + ) + if (!skip.has(kind)) + tokens.push( + kind === ts.SyntaxKind.StringLiteral + ? JSON.stringify(scanner.getTokenValue()) + : // Formatting also reflows the HTML in a component template. + scanner.getTokenText().replace(/\s+/g, ``), + ) + return tokens.join(` `) +} const hashOf = (code) => - createHash(`sha256`).update(code.trim()).digest(`hex`).slice(0, 16) + createHash(`sha256`).update(code).digest(`hex`).slice(0, 16) function walk(dir, keep) { if (!existsSync(dir)) return [] @@ -92,6 +118,8 @@ for (const file of markdown) { } } +for (const snippet of snippets) snippet.hash = hashOf(fingerprint(snippet.code)) + if (process.env.SNIPPET_ONLY) { const keep = snippets.filter( (s) => `${s.file}:${s.line}` === process.env.SNIPPET_ONLY, @@ -292,10 +320,10 @@ snippets.forEach((snippet, i) => { }) const known = (snippet) => - baseline[snippet.file]?.includes(hashOf(snippet.code)) ?? false + baseline[snippet.file]?.includes(snippet.hash) ?? false const newFailures = failures.filter(({ snippet }) => !known(snippet)) const failing = new Set( - failures.map(({ snippet }) => `${snippet.file}#${hashOf(snippet.code)}`), + failures.map(({ snippet }) => `${snippet.file}#${snippet.hash}`), ) const stale = Object.entries(baseline).flatMap(([file, hashes]) => hashes @@ -306,7 +334,7 @@ const stale = Object.entries(baseline).flatMap(([file, hashes]) => if (updateBaseline && !process.env.SNIPPET_ONLY) { const next = {} for (const { snippet } of failures) - (next[snippet.file] ??= []).push(hashOf(snippet.code)) + (next[snippet.file] ??= []).push(snippet.hash) for (const hashes of Object.values(next)) hashes.sort() const sorted = Object.fromEntries( Object.entries(next).sort(([a], [b]) => a.localeCompare(b)), diff --git a/scripts/doc-snippets-baseline.json b/scripts/doc-snippets-baseline.json index 6cf6a5be54..d2b15d82fc 100644 --- a/scripts/doc-snippets-baseline.json +++ b/scripts/doc-snippets-baseline.json @@ -1,273 +1,273 @@ { "docs/collections/electric-collection.md": [ - "0dd50060adbf8c3f", - "102e3b6860d47f70", - "54275ea6e4b24910" + "56d9efac4ed9aa8d", + "7422149f2dff594a", + "b16254176b96780a" ], - "docs/collections/indexed-db-collection.md": ["dad4e77c4763f32a"], + "docs/collections/indexed-db-collection.md": ["02840970e37fce46"], "docs/collections/local-only-collection.md": [ - "35d97a3a9d141256", - "3615b30b88224e39", - "36f0f65995ced962", - "62bfffbdd760af64", - "78e9144a8c4d6946" + "1616bfc9a1c371dd", + "23aedda50d1a9c4d", + "9b19823410809fa7", + "ab45c1f424a4f5c2", + "fc371ec14b35d52c" ], "docs/collections/local-storage-collection.md": [ - "22017ba94407fe7d", - "2add0a25c7533df9", - "3663c960d1461dde", - "7a5ee2385c57e6da", - "93529b815b56005f", - "952d65ad77259ef8", - "a23b02c8ebff50ea" + "000e5e37db6d115b", + "44ba7d96e999cffd", + "517ab246f2d8be78", + "bc535604c77c1fa1", + "c38cafc80222d29c", + "dd2763deb14376a3", + "eabd7079c8e94cee" ], "docs/collections/powersync-collection.md": [ - "1afab707dfed2b14", - "20090ec993fa884d", - "4dd21379bff94d51", - "54e13a571814f441", - "b7a80e0fdee7efdc" + "08fdc12554aef6bd", + "0ed5441c16943c10", + "2b5c40bcc4e20ec0", + "a0537dcb94f1fdd8", + "f40497dfb722c9dd" ], "docs/collections/query-collection.md": [ - "0c7c57c2f2daf128", - "25caab8b5e0160ca", - "40db37e1b3b1649f", - "46c646552bb94fa1", - "46eafbc648a2de41", - "4936e66d71ebc0b5", - "79702ff5b213cb74", - "7ec88a76700c9ef5", - "8f3f399ac425424a", - "947554ae2af1c47d", - "9d6166c311d0d6e6", - "a3687d66ef982466", - "a5b52c9f8af29d00", - "e14f245e9749d168", - "ea2ff4dd09fbb02f" + "0229dd13bd4d1ffd", + "061e910f0c004a30", + "1b699b36eff1d332", + "2574bda356de34f2", + "2b6afbd876364c47", + "393233dab9f06f9f", + "40cb3614551740e2", + "7629a8f334d06ac6", + "7fa66ee20b7bfb43", + "8f2171f6414ed15e", + "958f9a40aa89f03c", + "af597bdfb912efff", + "bd88200750280a4c", + "c70312226e5685fc", + "e45e143986a0a5c5" ], "docs/collections/query-initial-placeholder-data-design.md": [ - "9edcfc23f56a849b" + "149bb533130822d5" ], "docs/collections/trailbase-collection.md": [ - "01bc2c23f090ee82", - "8e6131fa04843059", - "c5e5da6b0528d05a", - "da0fb4a61a4b1258", - "e5fd5fd7cf3ee7af" + "6ce5b7286c00a06c", + "a3a582bdac23bba1", + "d87926f88d1893b3", + "f52226428b04d159", + "fe61c9648fce3ad9" ], "docs/contributing/oracle-reviews/initial-full-source-sync-readiness.md": [ - "db1f0953a754e669" + "e8bffd051d9cc384" ], - "docs/framework/angular/overview.md": ["d39903c5982ee517"], - "docs/framework/react/overview.md": ["74b26b1d16180ce0", "b0e6d12ea9882897"], - "docs/framework/solid/overview.md": ["505fbc1660154c2b"], + "docs/framework/angular/overview.md": ["608090f6634091b7"], + "docs/framework/react/overview.md": ["4b92baa28023107a", "c454038dbd572c86"], + "docs/framework/solid/overview.md": ["0ed5a4dc482e3487"], "docs/guides/collection-options-creator.md": [ - "26cf7b70bf5a9328", - "6555958fbcbbe3f3", - "6b31a02781cc0b6f", - "7bec8c32bbcdd756", - "7ef713bfd5fc90d4", - "8bad2af899e2098a", - "a04db2d01d988b05", - "ada9c5856d6af91e", - "e31e1498002eef0b" + "1e45d0804bc49c6d", + "30721b8dbc34c240", + "30959fbff6b0b9b3", + "8e4594291426b79a", + "94e97da76ab64521", + "98f8d2a6f39e3d08", + "a56d2489cdbbbe2d", + "ad2c8cf0431a5130", + "d04164c069c046e7" ], "docs/guides/error-handling.md": [ - "11d18030135ddad1", - "2562d7c6eeede61b", - "2dce561a1a49b614", - "339019147dd88b41", - "392bbc3bc9e5790c", - "3a3ba04c9ccdeb61", - "3f29a3e83096bccb", - "73bf36e975556612", - "82a5eb0bd5e837b4", - "adaec9d8439a0d76", - "c2db832bb6875e28", - "c740378204672b1c", - "d92784296ae4c98a", - "e647ec0d24c013f6", - "f5a0e5835193f709" + "0d42062acd306170", + "21c4f07ed33e73d3", + "2e4f4ffd14769f71", + "39b87a75c1a7cddf", + "39d852db0f643223", + "45b7505ca2e2d743", + "51e4e1b10b424521", + "80f1e12fddaac9ad", + "94f75b445e4e77e3", + "a13c70241881d8bb", + "a227bec199f61386", + "b1ed7616c9a09f1e", + "b3b928d3f7fd310e", + "b9b81423f794c4a8", + "d1c64f9173ac52a0" ], "docs/guides/live-queries.md": [ - "01f663e8b4b7a5c5", - "03b16ea37af5155a", - "193a6540c5cb9751", - "3168fcfa7b402bd5", - "3c7650d829b70c58", - "41ad2967e4792981", - "44a43649d5328fee", - "5049db06a1bca6c2", - "54b2845b3f4a3a94", - "5fcce92ba0507289", - "70bbc60053a33fad", - "a4cbec996c7c4cb5", - "bdc9c30c57add7cd", - "bfaf11792701b5a6", - "cdddcc4e9dff0a9b", - "d42c540f286dedc0", - "d5628fff0679e5dc", - "f113d47460e2166b", - "f4ba391e288f96cc" + "012683b68aea9bd9", + "1c901a9aab4c3605", + "328e6118b6859a6d", + "350c201eb314ac4a", + "3606a1edf6576b6b", + "47c25de745935ecf", + "49792941ddf68946", + "49c11d80e5db559e", + "4d22db9980cce9ba", + "539715bcca864ac7", + "835b83a3af148c5d", + "8590ef81b4186488", + "9d9f28d1f7ce5e67", + "9e5a76892e694d3f", + "acdeadcf40bb3576", + "c561ebd715ee0dc0", + "d1a56dd2b016d61c", + "ef28fad1f061708d", + "f5dfaaee1c2fc9b1" ], "docs/guides/mutations.md": [ - "08261373bf652f48", - "1255521980bf984d", - "1397fc8a87edfa8b", - "1b9ddb3f971078cc", - "2e74aee8c03a2c23", - "3ed04a2948b0b6e0", - "479ded16124a4b79", - "6ab013c30a539cb0", - "6e5d7d2bc0e7944a", - "779413de3b4085a2", - "7e5e18900577074f", - "839efb04d2715746", - "8c968ac8b3f2b84f", - "8d38c4925d5a1918", - "ad561652b2f92f13", - "be1a2f6b93290baa", - "c3e85f5abfc6333d", - "cecaaeb48d5e7a8b", - "e48a63a8418dd5f6", - "fffbd38b18827d0e" + "10504e85bc44cb36", + "23c224b97339aad2", + "24f926bb7f5e9bf4", + "4c84ab8dcd0c869b", + "5405b8237edb7edc", + "621ca9c1abe615b2", + "676555290fb5f3ec", + "6b6fed7dbce90a71", + "71b81aa657f0792a", + "7272172699ac9d1f", + "763d3f2ff4f82ff8", + "98f2cab14439b918", + "9b556db14527fe01", + "bc7f2418773fce1c", + "c2461550f9d3e496", + "de14bc736e2ed20c", + "e01722dd826edfe8", + "e1e4ae286396b7b5", + "e79a4c90cf8a5761", + "ff2316ae59ea3bd0" ], "docs/guides/offline-transactions.md": [ - "24e0163dcb007fab", - "8d210fead79f3cf0" + "53fad67bfc557743", + "cbb84bbc032fe6d9" ], "docs/guides/schemas.md": [ - "136aaed9cbe5ea61", - "19b138ed11f82f63", - "21c20e07e4bbc178", - "2320661c518a4ad3", - "2f838bf85e0da434", - "492dbb0212c677de", - "7019266cd52d2ed5", - "877b3a9c16c15da2", - "9ceb0ac6e0564630", - "aadec84ec61f60f1", - "dbf1970a78767f17", - "ebed33ff5f590c48", - "f00d518c1dc0f352" + "0f1234ee3709c004", + "175f5a2c0175199c", + "4feb48bc8c2450d8", + "50b51df6b24125cc", + "571063c019efde35", + "5b2a85437b077d70", + "6ff4f1903d62e884", + "9ce8558a3c61a956", + "b30da55c0692c68c", + "caf0b926ff1b6e4d", + "df0dab60bf88a19b", + "ec0faf98a535caac", + "f9cec253d1e29e88" ], "docs/guides/sqlite-persistence.md": [ - "0bc141ca7a9cecaa", - "76993422b4c8881f", - "be59956fa556b952", - "dcf611fc0de7a5ea" + "44d938b27e7d8e56", + "6b656ba540d345bd", + "dca01a57fc0a2e04", + "fb16035447c1c5b4" ], "docs/guides/ssr.md": [ - "2eeb8be63193e90e", - "3f521af644cc4a6c", - "7df90009f17bb64f", - "bae874816b80fa76", - "d0c5531a14522216" + "43ecdc891c511196", + "7d6d492c15650152", + "86955b615a421d2f", + "bb340f403f9813b1", + "c809652214430dfa" ], "docs/overview.md": [ - "17addcbdcdc8bd9d", - "25834862ad19e87b", - "49c4d17ae089392e", - "6b316852f49bd7e1", - "7f2e477746fb333c", - "b218fcb90eae1b25" + "29c08f39ce288ed8", + "3a15ab6c1b66500c", + "bccbf6aceee45753", + "be08f67a8db26760", + "c31cc7218909bb3f", + "c64c851e1a775b7d" ], "docs/quick-start.md": [ - "031935aa43874a0a", - "3dc699a8394a7d6d", - "73d5900ac8e2d76b", - "74b26b1d16180ce0" + "050709d610c65189", + "4b92baa28023107a", + "9a2229c6576082a7", + "b6f8297128236baf" ], "packages/angular-db/skills/angular-db/SKILL.md": [ - "0039ec2323554d77", - "1c2d766183b1c4ed", - "2170cb84aea1c46e", - "2e9b57d202697885", - "373e31c9185d8b2d" + "05e664d90130374e", + "52cc4733bc84ecdc", + "70be2e9d955c38d4", + "a383923cc25453f2", + "d7162b5c2af01089" ], "packages/db/skills/db-core/collection-setup/references/local-adapters.md": [ - "131410fc478710cc", - "1a15d24213289e11", - "ba6f6ce87766c12f", - "fb57b98d2e7f574a" + "4efbd8925c757c30", + "626940148a94ad34", + "d9d940252f67e38d", + "f9bb0de5ca97a91d" ], "packages/db/skills/db-core/collection-setup/references/powersync-adapter.md": [ - "9667df44886f0ca2" + "ea56bb18de1e5bc3" ], "packages/db/skills/db-core/collection-setup/references/query-adapter.md": [ - "458f58547abe2d59", - "4f6f60340f2e2c0b", - "8a1c6eada500cff8", - "9b0bf4836d0cfb78", - "b6712a06f76aabeb", - "c034a05d65b63d4a", - "d1afd54756a54a67" + "1d3913d423bc7e38", + "3b24ad26456d3e7c", + "591f73320d6029a0", + "5f24bed8eb699df8", + "9c1631a553ea07d2", + "ac10737f5050571f", + "fcf5567666226944" ], "packages/db/skills/db-core/collection-setup/references/trailbase-adapter.md": [ - "049d5225d8e5e24a" + "e0e8861a744963a2" ], "packages/db/skills/db-core/collection-setup/SKILL.md": [ - "0ce632547d2f3429", - "1860f97d7af4d664", - "2045994875b4948a", - "4bdb69d627811855", - "9760d69c1a43223c", - "b6846dcda912e886", - "c529358f3d493b5a", - "cc984debd9673623", - "ebca8a1eea44c425", - "ef0695b2edac1d47", - "f807227957b45b54" - ], - "packages/db/skills/db-core/custom-adapter/SKILL.md": ["b3c120c7a08c2366"], + "070f001f9160a30b", + "26751817f0f8e23a", + "591e4f7c004f672d", + "84f362c63e9360cb", + "9eabaf549c35dd7a", + "a8fde4fbfc21276a", + "abba53377897fe5e", + "af80855f8b6db66c", + "c83cd2bf72d16c54", + "cf64631446425ad8", + "ea4b823c98753caa" + ], + "packages/db/skills/db-core/custom-adapter/SKILL.md": ["0946d1e769d92cf7"], "packages/db/skills/db-core/live-queries/references/operators.md": [ - "4b17b68395b1fe83" + "012683b68aea9bd9" ], "packages/db/skills/db-core/live-queries/SKILL.md": [ - "32a783108e6f18bf", - "738472679137647d", - "918167034910a1e9", - "b1246c8734027a6a" + "069a9d0f7d5744b2", + "70da1f691de74854", + "d808ae7acfd79f4d", + "fcf8dccac11c4ade" ], "packages/db/skills/db-core/mutations-optimistic/references/transaction-api.md": [ - "e35c8663478b601a" + "967d90e913132656" ], "packages/db/skills/db-core/mutations-optimistic/SKILL.md": [ - "1337a5a5bcd052bd", - "4f3879defa7fb44f", - "aebe19060ddcbd7c", - "d289f1c191a383e3", - "e720a019114309a8" + "1dbef3144d02599f", + "549bc78a7c315a5d", + "6b0a0344d218354e", + "8b95067b56d2d9ca", + "a87b1b6b7f312e36" ], "packages/db/skills/db-core/persistence/SKILL.md": [ - "132b43a15b4c1d96", - "65b4f94b03be5df4", - "6a97114189308f12", - "8e483f20ce54a2cd", - "ba99444cf808cdc1", - "e17e1d3eedb30ae4" - ], - "packages/db/skills/meta-framework/SKILL.md": ["77fd28263fca116b"], + "226a17d946d2644a", + "42ca999e9550faf3", + "4321c008910761e3", + "49cc6207176cfdd0", + "6e51303b6511a747", + "9d1be38d645f5ff3" + ], + "packages/db/skills/meta-framework/SKILL.md": ["29bfd0d0914b4128"], "packages/offline-transactions/skills/offline/SKILL.md": [ - "120091041b09393e", - "1a506e324de141a4", - "4bb0f7db77cc9451", - "dbd64ce5b54e79cf" + "14055dbaded0d439", + "17d601a70493c3b0", + "9a3cd51e9c17d3f9", + "ba3fc8531b556c3e" ], "packages/react-db/skills/react-db/SKILL.md": [ - "23255f3913c6cc54", - "5828824fffb42015", - "6cea34f4dcc65cae", - "e18a2c5d10ddc044" + "25034f3ee0fa0a2b", + "2ef25b5bf5bff30f", + "313daeae3a103f55", + "c878afe32da4edee" ], - "packages/solid-db/skills/solid-db/SKILL.md": ["fc051f29c5d4f27e"], + "packages/solid-db/skills/solid-db/SKILL.md": ["7860a5e90890c302"], "packages/svelte-db/skills/svelte-db/SKILL.md": [ - "502c3a8c40af4e0d", - "56cbaae9c63c2fc2", - "ed4d80558901d985" + "5c9035dd0f4aad97", + "72cc3552d6a4ade1", + "9f5ba57807c8dd03" ], "packages/vue-db/skills/vue-db/SKILL.md": [ - "4cbe8baab108bd25", - "8bcb70ab4bdfacfc" + "23d62290a7b76578", + "bb0769916fc9762b" ] } From ec50aae869e1be7439ef6383eadf640afcdd179c Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 23 Oct 2026 10:09:34 -0600 Subject: [PATCH 19/32] test: check opt-in pooling in every adapter input form The conformance suite checked pooling only in the config form with pool. Two scenarios now run in every framework driver: - pool-opt-in: queries without pool keep a source subscription each in the function, config, and parameterized forms. - pooled-param-change: a pooled parameterized query follows its parameter between pool groups beside an unchanged peer, and releases its subscription on unmount. mountControllable takes an optional pool, and each driver uses its adapter's config form for it (React without deps, Angular params). Pooling every query by default, a pooled identity that ignores literal values, and an Angular params form that drops pool each fail them. Co-authored-by: Isaac --- docs/contributing/oracle-coverage.md | 2 +- packages/angular-db/tests/conformance.test.ts | 2 + packages/db/tests/conformance/contract.ts | 5 +- packages/db/tests/conformance/suite.ts | 91 ++++++++++++++++++- packages/react-db/tests/conformance.test.tsx | 8 +- packages/solid-db/tests/conformance.test.tsx | 11 ++- .../tests/conformance.svelte.test.ts | 8 +- packages/vue-db/tests/conformance.test.ts | 8 +- 8 files changed, 123 insertions(+), 12 deletions(-) diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index d2b6c98a82..aabdad3e61 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -650,7 +650,7 @@ generic visitor behavior outside the named collectors. | WHERE predicate publication | [WHERE predicate publication oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/where-predicate-publication-oracle.property.test.ts), [`in` evaluator](https://github.com/TanStack/db/blob/main/packages/db/tests/query/compiler/in-evaluator-oracle.property.test.ts) ([review](oracle-reviews/perf-in-constant-set.md)) | An independent Kleene evaluator judges `eq`/`not`/`and`/`or` predicates over strings, a normalization-prefixed string, booleans, numbers, `NaN`, a valid Date, `null`, a missing field, and virtual fields. Snapshot histories with pending optimistic inserts compare a live query, direct subscribers with and without initial state, and `currentStateAsChanges`; change histories apply multi-key sync transactions, including reinsertion of a key deleted earlier, and compare every consumer's key set after each commit, on scan and `BasicIndex` paths. Peer subscribers share one field with different literals, plus one on a virtual field, so every published change must reach exactly the subscribers whose predicate it can satisfy; routing mutants that ignored the previous value, ignored the field, delivered a change twice, or routed while stale published rows awaited reconciliation fail here. Fixed witnesses cover a layout-only publication reaching a filtered subscriber as one empty batch, a filtered subscriber's empty Collection-readiness batch, retraction of a vanished row after eager cleanup and restart, and the new row plus exact subscriber update payload when a same-key row stays TRUE. A stale-live-value mutant survives the generated membership checks but fails the payload witness. A focused [property-visibility test](https://github.com/TanStack/db/blob/main/packages/db/tests/query/where-prefilter-property-visibility.test.ts) compares the public unindexed snapshot with the enriched-row contract for inherited, non-enumerable, enumerable-own, and nested getter paths; the pre-fix stored-row shortcut failed three of its four controls. Other property-descriptor and stateful-getter histories remain outside those fixed controls. Hostile mutants for FALSE-for-UNKNOWN `eq`, `or` or number-literal subscription prefilters, a prefilter that ignores the previous value, a skipped readiness batch, a skip while stale published rows await reconciliation, and an unindexed snapshot scan that reads only synced rows while an optimistic insert or delete changes visibility passed the prior `@tanstack/db` suite and fail here. Change routing withholds a dropped row from an `eq` subscription before its sent key could be recorded, so a mutant that records dropped rows as sent is equivalent for routed predicates in this oracle. A focused `collection-subscription.test.ts` witness checks that a dropped insert or update, alone or beside a matching change, cannot advance a limited subscription's page offset under a routed `eq` and an unrouted `or` predicate; the unrouted cases beside a matching change kill that mutant, and the unrouted `alone` case does not. A loose-equality prefilter is an equivalent mutant: it only skips less. Skipping during truncate replay also survives; the replay's baseline diff re-derives the same retraction, so the guard keeps the prior dataflow without its own witness. Generated cleanup and restart histories for filtered subscribers remain open for the lifecycle publication owner. No-op updates, other comparison operators, Temporal and binary operands, joins, ordering, optimistic updates, and truncate are outside this owner. It runs in `@tanstack/db`'s `test:oracles` campaign. The [2026-09-30 review](https://github.com/TanStack/db/blob/main/docs/contributing/oracle-reviews/2026-09-30-where-predicate-and-join-keys.md) records each ORC outcome. Change routing was later removed in favor of pooled live queries; the routing mutants above are historical, and the same peer laws now check plain subscription dispatch. The unindexed snapshot prefilter was restored for compiled queries as one stored-row `eq` test on a string or boolean literal. Its property-visibility test also covers the reverse direction: a stored row with an inherited or non-enumerable field must still match `isUndefined` on that field, which a scan that evaluated the full predicate on stored rows fails. The `in` evaluator owner checks `in` against an independent linear reference over Dates, `NaN`, `-0`, BigInt, bytes, Temporal values, objects and nullish items, for constant and row-supplied lists. | | Joined result keys | [Joined result key oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/join-result-key-oracle.property.test.ts) | A nested-loop model recomputes the pair set of an inner, left, or full join over source keys drawn from plain, comma-bearing, bracket-bearing, and quoted strings, numbers beside the strings that print the same, both infinities, and `NaN`. After preload and each synced group change, the published rows must equal the model's pairs and the result key count must equal the row count. The comma-joined key encoding fails its two pinned histories and both campaigns; plain `JSON.stringify`, which prints infinities as the missing-side `null`, fails the pinned infinity history and both campaigns. A pinned history in which a `NaN`-keyed pair leaves and re-forms fails when the join index compares source-key prefixes with `===`. Joins over subqueries, more than two sources, custom `getKey`, and optimistic mutations are outside this owner. It runs in `@tanstack/db`'s `test:oracles` campaign. The [2026-09-30 review](https://github.com/TanStack/db/blob/main/docs/contributing/oracle-reviews/2026-09-30-where-predicate-and-join-keys.md) records each ORC outcome. | | D2 Index storage | [Index refinement oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/index-refinement-oracle.property.test.ts) | A plain-`Map` model sums multiplicities under a type-and-value identity in which `NaN` equals `NaN` and `-0` equals `0`. Histories of up to twelve additions to two keys mix prefixed arrays with `NaN`, `0`, `-0`, `1`, `'1'`, `1n`, and `'a'` prefixes and unprefixed values, with cancellation and reappearance. After each addition, `get` and `has` must equal the model. Each step also checks that `get` returns what `getIterator` yields, in its order, since the two walk the storage separately. Comparing prefixes with `===`, which disagrees with the `Map` that holds them, fails three pinned histories and both campaigns; the `-0` control passes under both. Every history also runs with `prefixIdentity`, where values under one key with the same prefix are one value; mutants that merge unprefixed values or ignore the option in a prefix map fail. The [structural identity oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/structural-identity-oracle.property.test.ts) checks that `structuralIdentity` agrees with D2 value identity for equal representations (array subclasses, class instances, null prototypes, key order, non-enumerable symbols) and independent values, including separator-bearing strings and keys, enumerable symbol keys, Dates, RegExps with numeric `lastIndex`, Maps and Sets with zero to two entries (the second Set member is a fresh tagged array, so primitive deduplication and unwrapped reordering are outside this lane), arrays with an optional enumerable `note` field, binary values, holes, deep nesting, and a cyclic value; an independent normal-form model decides equality and is checked against `equalHashValues`. The model expands `deep` shorthand into nested arrays. A pinned equal `deep`/array pair and adjacent-depth control guard the model boundary exposed by seed 1158727847/path 255. The equal-pair grammar crosses same-spec values with explicit-array/`deep` equivalent specs; the independent-pair grammar uses two specs. Both run fixed and fresh campaigns with guarded one-case seed/path replay. Pinned same-description symbols and equal-body Map/Set/object pairs expose collisions that random independent pairs rarely reach. Mutants that stringify symbols, collapse those three object-kind tags, drop a string's or key's length or an array's length, or omit Date and RegExp headers fail it. A first-entry-only Map/Set mutant and an array-identity mutant that ignores enumerable non-index fields fail the pinned comparisons; lowering the depth cap is equivalent, because the cap only bounds work. Open: reference-valued RegExp `lastIndex` can give D2-unequal values one defined identity and merge `groupBy` contributions ([#2192](https://github.com/TanStack/db/issues/2192)); the structural-identity and incrementalization owners need a production repair and receiving witness. Temporal values, other opaque reference values, large binary values, getters/proxies, and mutation between observations are outside this generated grammar. groupBy's use of it is owned by the incrementalization law: its string-extrema lane fails a string identity without a length prefix, and a focused groupBy witness fails when the group key is left out of the identity. Compaction, presence tracking, and structural payloads are outside this owner; a focused test checks that an index built without presence (as reduce builds its indexes) throws when presence is read; the [reduce fast-path oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/operators/reduce-fast-path-oracle.property.test.ts) compares the fast path's emitted entries one by one with the general path's, over keys that move between the paths, hash-merged outputs, -0, and zero and negative multiplicities, while an unchanged `NaN` output is owned by a focused reduce test; the join operator tests and incrementalization law own operator behavior. | -| Pooled live queries | [pooled live query oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/pooled-live-query-oracle.property.test.ts) | An independent `eq` model (nullish UNKNOWN, Date as timestamp, `NaN` equal to `NaN`, `-0` equal to `0`) gives membership; a live-query Collection compiled for the same query gives order, values, and status. Histories mix sync transactions, optimistic writes confirmed or rolled back, peer mounts and unmounts, and source cleanup and restart, including cleanup while a write is pending and a mount between cleanup and restart. Wholesale observers must match after every step, granular changes must match a reference observer by type, key, value, and previous value and replay to the model's rows, the layout revision must advance when keys reorder, and observing must never build the live-query Collection. Peers may add a residual `not(eq(...))` conjunct, which the model evaluates itself; a view that ignores it, reports an unfiltered `entries()`, or delivers a row entering or leaving it as an update fails it. Other residual operators use the same compiler evaluator but are not generated. Peers may also order their rows by `id` or by a `g` that can be null, with explicit `nulls`; order comes from the reference. A comparator that ignores direction or `nulls`, an order left out of the partition key, and key order instead of the query's order each fail the pinned reorder history and both campaigns. A partition that ignores a row's previous group, orders by arrival, skips normalization, skips the initial snapshot, follows the source's status after cleanup, or never terminates fails it. Two shared conformance scenarios (`eq-filter-rows`, `eq-filter-peers`) run the pooled path under React, Vue, Solid, Svelte, and Angular; a partition that ignores a row's previous group fails both under every adapter, and `eq-filter-peers` checks through `subscriberCount` that each adapter shares one source subscription per partition while mounted and releases every source subscription after the final framework unmount. A focused [release-timing test](https://github.com/TanStack/db/blob/main/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts) compares when a pooled query and a live-query Collection release their sources across `gcTime` 1, 100, 0, and `Infinity`, with and without a subscriber, and with no `gcTime`, where both use the live-query Collection default, checks that a partition waits for its views' longest `gcTime`, checks that a view resubscribed after its partition released follows later source writes as its live-query Collection does, including a view with a residual conjunct, and checks that a partition that subscribes again still serves new mounts from one source subscription. Further focused witnesses check that a partition keeps groups only for values that have rows or watchers, that a detached reader never sees a dropped group's revision reused, keep a pooled query's public Collection live while the view is observed, accept it as a query source, and check that a pooled query turns terminal when source cleanup starts while the adapter's cleanup is still pending, as a live-query Collection does. A pinned oracle witness checks that an `eq` path whose getter throws excludes the row on both paths; a pooled path that rethrows fails it. The pooled oracle's grammar never releases a partition, so release and resubscribe histories rely on these fixed witnesses; a fixed delay, a missing unsubscribed floor, a last-`gcTime`-wins rule, and releasing at `gcTime` 0 each fail it. Removing the partition's final-listener release survives the mounted row checks but fails the shared framework unmount checkpoint and core release-timing owner. The [2026-10-01 review](oracle-reviews/2026-10-01-pooled-live-queries.md) records each ORC outcome. | +| Pooled live queries | [pooled live query oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/pooled-live-query-oracle.property.test.ts) | An independent `eq` model (nullish UNKNOWN, Date as timestamp, `NaN` equal to `NaN`, `-0` equal to `0`) gives membership; a live-query Collection compiled for the same query gives order, values, and status. Histories mix sync transactions, optimistic writes confirmed or rolled back, peer mounts and unmounts, and source cleanup and restart, including cleanup while a write is pending and a mount between cleanup and restart. Wholesale observers must match after every step, granular changes must match a reference observer by type, key, value, and previous value and replay to the model's rows, the layout revision must advance when keys reorder, and observing must never build the live-query Collection. Peers may add a residual `not(eq(...))` conjunct, which the model evaluates itself; a view that ignores it, reports an unfiltered `entries()`, or delivers a row entering or leaving it as an update fails it. Other residual operators use the same compiler evaluator but are not generated. Peers may also order their rows by `id` or by a `g` that can be null, with explicit `nulls`; order comes from the reference. A comparator that ignores direction or `nulls`, an order left out of the partition key, and key order instead of the query's order each fail the pinned reorder history and both campaigns. A partition that ignores a row's previous group, orders by arrival, skips normalization, skips the initial snapshot, follows the source's status after cleanup, or never terminates fails it. Two shared conformance scenarios (`eq-filter-rows`, `eq-filter-peers`) run the pooled path under React, Vue, Solid, Svelte, and Angular; a partition that ignores a row's previous group fails both under every adapter, and `eq-filter-peers` checks through `subscriberCount` that each adapter shares one source subscription per partition while mounted and releases every source subscription after the final framework unmount. A focused [release-timing test](https://github.com/TanStack/db/blob/main/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts) compares when a pooled query and a live-query Collection release their sources across `gcTime` 1, 100, 0, and `Infinity`, with and without a subscriber, and with no `gcTime`, where both use the live-query Collection default, checks that a partition waits for its views' longest `gcTime`, checks that a view resubscribed after its partition released follows later source writes as its live-query Collection does, including a view with a residual conjunct, and checks that a partition that subscribes again still serves new mounts from one source subscription. Further focused witnesses check that a partition keeps groups only for values that have rows or watchers, that a detached reader never sees a dropped group's revision reused, keep a pooled query's public Collection live while the view is observed, accept it as a query source, and check that a pooled query turns terminal when source cleanup starts while the adapter's cleanup is still pending, as a live-query Collection does. A pinned oracle witness checks that an `eq` path whose getter throws excludes the row on both paths; a pooled path that rethrows fails it. The pooled oracle's grammar never releases a partition, so release and resubscribe histories rely on these fixed witnesses; a fixed delay, a missing unsubscribed floor, a last-`gcTime`-wins rule, and releasing at `gcTime` 0 each fail it. Removing the partition's final-listener release survives the mounted row checks but fails the shared framework unmount checkpoint and core release-timing owner. The shared adapter conformance suite checks the opt-in boundary in every framework: queries without `pool` keep a source subscription each in the function, config, and parameterized forms, and a pooled parameterized query follows its parameter between pool groups beside an unchanged peer. Pooling every query by default, a pooled identity that ignores literal values (React then reuses the old query), and an Angular `params` form that drops `pool` each fail it. The [2026-10-01 review](oracle-reviews/2026-10-01-pooled-live-queries.md) records each ORC outcome. | | Live-query deferred acquisition | [deferred acquisition oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts), [React Suspense warm-cache witness](https://github.com/TanStack/db/blob/main/packages/react-db/tests/suspense-warm-cache.test.tsx) | An independent model of the law that a live-query Collection starts no provider work before a subscriber or a preload in its current sync run crosses five source states (eager or on-demand, idle or already started, and an on-demand source that already holds the rows), an unordered query and an ordered window, one or two live-query levels, and seven histories: start and read, subscribe or preload after a start, subscribe or preload without one, cleanup before a subscriber, and restart after a run that had one. After every command it checks source sync-run starts, acquisition attempts, readiness when the call returns (the synchronous observation cut), settled readiness, and rows. A pooled block checks that building a pooled view starts no idle eager source until a view subscribes or preloads, and a DbClient block checks that a preload answered by a pending or settled stream still resumes acquisition. Mutants that never defer, keep the subscriber-or-preload fact across cleanup, ignore preload, never resume a builder's subscriptions, let a deferring subscriber start a base source, build a pooled partition without deferral, never resume it, mark a preload one microtask late, or drop the stream's preload mark each fail it. The React witness checks that `useLiveSuspenseQuery` over a source that already holds the rows renders without the fallback, for eager and on-demand sources and both shapes; creating the Suspense collection started, or not preloading it in render, fails it. A later review added `peer` and `truncate` commands: a peer that starts an idle on-demand source, or a truncate on a running one, made the live query acquire before any subscriber, and both guards are killed separately. Pinned blocks check that a subscriber surviving cleanup is served after a restart while a surviving deferring subscriber is not, and that `toArrayWhenReady()` and `stateWhenReady()` count as a preload. A pinned block checks that a deferring live query is not counted in its source's `subscriberCount` or `subscribers:change` until its first subscriber, while still keeping the source from garbage collection; a Query Collection test checks that it causes no refetch. Another pinned block checks that a preload or subscriber requested by an inner live query's status handler, while the outer query creates its subscription to it, still resumes acquisition. Solid's Suspense receiving witnesses check that a resource whose preload settles first retains its live-query Collection while its sibling remains pending past the unsubscribed floor, then releases that temporary owner on both commit and abandonment, and that a preload settling after its owner unmounts cannot publish an error through the abandoned result. Open: source failure and asynchronous loads before resumption, which the subscription lifecycle oracle owns once acquisition resumes; a resume that throws, which leaves later sources unstarted only for a live query that is already in error; asynchronous sources; and Suspense with a `DbClient`, where render already defers source starts. | | Live-query notification algebra | [real-query and checker calibration](https://github.com/TanStack/db/blob/main/packages/db/tests/notification-laws-oracle.test.ts), [independent Map checker](https://github.com/TanStack/db/blob/main/packages/db-collection-e2e/src/utils/notification-laws-oracle.ts) | One-key synchronous source histories check exact live-query callback reconstruction and unsubscribe silence. Synthetic multi-key histories calibrate the checker; direct source Collection events remain with the change-event history owner. | | Flat-row change tracking | [flat change tracking oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/flat-change-tracking-oracle.property.test.ts) | Generated callbacks over flat rows, frozen or not and with or without a non-enumerable field (assignments of `0`, `-0`, `NaN`, `undefined`, functions, and fresh objects; reverts; deletions; added fields; data and accessor `defineProperty`; stored drafts; throws), run through the flat tracker, the draft proxy, and an independent model, which must report the same change sets with `0` and `-0` equal, or throw the same error and leave the rows unchanged. Defining a field acts as assigning it, and a non-enumerable field is row data only once written; the proxy broke three of those laws on `main` and the flat tracker one. Diffs using `!==`, `Object.is` alone, or no deletions fail it. It found that the proxy dropped a field added as `undefined` when another field reverted; the pinned history and both campaigns fail without that fix. Non-flat rows must fall back. The [2026-10-01 review](oracle-reviews/2026-10-01-pooled-live-queries.md) records each ORC outcome. | diff --git a/packages/angular-db/tests/conformance.test.ts b/packages/angular-db/tests/conformance.test.ts index e374b55157..a1acb9fdc8 100644 --- a/packages/angular-db/tests/conformance.test.ts +++ b/packages/angular-db/tests/conformance.test.ts @@ -215,12 +215,14 @@ function mountDisabled() { function mountControllable

( build: (q: any, param: P) => any, initial: P, + opts?: { pool?: PooledLiveQueries }, ): ControllableHandle

{ const param = signal

(initial) const { result, destroy } = inCtx(() => injectLiveQuery({ params: () => ({ value: param() }), query: ({ params, q }: any) => build(q, params.value), + pool: opts?.pool, }), ) const handle = makeHandle(result, destroy) diff --git a/packages/db/tests/conformance/contract.ts b/packages/db/tests/conformance/contract.ts index bffb9e87ca..f401e69a8e 100644 --- a/packages/db/tests/conformance/contract.ts +++ b/packages/db/tests/conformance/contract.ts @@ -187,10 +187,12 @@ export interface LiveQueryDriver { /** * Mount a live query whose input depends on a parameter that can change after * mount. `build` returns a query, or `null`/`undefined` to represent disabled. + * With `pool`, use the adapter's config form for a parameterized query. */ mountControllable:

( build: (q: any, param: P) => any, initial: P, + opts?: { pool?: PooledLiveQueries }, ) => ControllableHandle

/** Mount a pre-created collection passed directly to the hook. */ mountCollection: (collection: Collection) => LiveQueryHandle @@ -216,7 +218,8 @@ export interface LiveQueryDriver { /** * Public sharing policy: queries whose config passes `pooledLiveQueries` * and that filter only by `eq` on one source share one source subscription - * per filtered field set, instead of one each. + * per filtered field set, instead of one each. Every other query has a + * subscription of its own: pooling is opt-in per query. */ pooledEqFilters?: boolean } diff --git a/packages/db/tests/conformance/suite.ts b/packages/db/tests/conformance/suite.ts index 314d98418d..1cb659ac88 100644 --- a/packages/db/tests/conformance/suite.ts +++ b/packages/db/tests/conformance/suite.ts @@ -34,7 +34,8 @@ * groupBy/aggregate, nested aggregates, `.includes` subqueries, findOne * cardinality, disabled + transitions, deferred readiness / eager / ready-with- * no-data, initial readiness over a lagging on-demand source, param recompilation, optimistic mutation, pre-created & config-object - * inputs, error status, and order-only moves. + * inputs, opt-in pooling in each input form, error status, and order-only + * moves. */ import { describe, expect, it } from 'vitest' import { @@ -127,8 +128,8 @@ export function runSuite(rawDriver: LiveQueryDriver) { sources!.track(rawDriver.makePrecreated(build, options)), makeErrorSource: () => sources!.track(rawDriver.makeErrorSource()), mount: (build) => track(rawDriver.mount(build)), - mountControllable: (build, initial) => - track(rawDriver.mountControllable(build, initial)), + mountControllable: (build, initial, options) => + track(rawDriver.mountControllable(build, initial, options)), mountCollection: (collection) => track(rawDriver.mountCollection(collection)), mountConfig: (build, options) => @@ -396,6 +397,88 @@ export function runSuite(rawDriver: LiveQueryDriver) { }, ) + // Pooling is opt-in per query. Queries that do not pass `pool` each + // subscribe to the source themselves, whatever input form they use. + scenario( + `pool-opt-in`, + `queries without pool keep a source subscription each`, + async () => { + const source = driver.makeSource(SEED) + const query = (team: string) => (q: any) => + q + .from({ items: source.collection }) + .where(({ items }: any) => ops.eq(items.team, team)) + const handles = [ + driver.mount(query(`a`)), + driver.mount(query(`b`)), + driver.mountConfig(query(`a`)), + driver.mountControllable((q, team: string) => query(team)(q), `b`), + ] + for (const h of handles) await h.flush() + expect(source.collection.subscriberCount).toBe(handles.length) + expect( + handles.map((h) => + (h.current().data as Array<{ id: string }>).map((row) => row.id), + ), + ).toEqual([[`1`, `3`], [`2`], [`1`, `3`], [`2`]]) + for (const h of handles) h.unmount() + }, + ) + + // A pooled query whose parameter changes leaves its old pool group and + // joins the new one: its rows follow the parameter, and a peer that keeps + // its parameter is not disturbed. + scenario( + `pooled-param-change`, + `a pooled query follows its parameter between pool groups`, + async () => { + const source = driver.makeSource(SEED) + const pool = { pool: pooledLiveQueries } + const query = (q: any, team: string) => + q + .from({ items: source.collection }) + .where(({ items }: any) => ops.eq(items.team, team)) + const moving = driver.mountControllable(query, `a`, pool) + const peer = driver.mountConfig((q) => query(q, `a`), pool) + for (const h of [moving, peer]) await h.flush() + const ids = (h: LiveQueryHandle) => + (h.current().data as Array<{ id: string }>).map((row) => row.id) + expect([ids(moving), ids(peer)]).toEqual([ + [`1`, `3`], + [`1`, `3`], + ]) + const shared = driver.features?.pooledEqFilters ? 1 : 2 + expect(source.collection.subscriberCount).toBe(shared) + + await moving.setParam(`b`) + await peer.flush() + expect([ids(moving), ids(peer)]).toEqual([[`2`], [`1`, `3`]]) + expectKeyedRows(moving.current().state, [SEED[1]!]) + expect(source.collection.subscriberCount).toBe(shared) + + source.insert({ id: `4`, name: `Dave`, age: 40, team: `b` }) + for (const h of [moving, peer]) await h.flush() + expect([ids(moving), ids(peer)]).toEqual([ + [`2`, `4`], + [`1`, `3`], + ]) + + await moving.setParam(`a`) + await peer.flush() + expect([ids(moving), ids(peer)]).toEqual([ + [`1`, `3`], + [`1`, `3`], + ]) + for (const h of [moving, peer]) h.unmount() + await expect + .poll(() => source.collection.subscriberCount, { + interval: 5, + timeout: 500, + }) + .toBe(0) + }, + ) + scenario(`live-insert`, `a sync insert appears in the result`, async () => { const source = driver.makeSource(SEED) const h = driver.mount((q) => @@ -1291,7 +1374,7 @@ export function runSuite(rawDriver: LiveQueryDriver) { ) it(`registers every distinct scenario without whole-test waivers`, () => { - expect(registry.size).toBe(37) + expect(registry.size).toBe(39) }) }) } diff --git a/packages/react-db/tests/conformance.test.tsx b/packages/react-db/tests/conformance.test.tsx index cf935439e9..0072952ca7 100644 --- a/packages/react-db/tests/conformance.test.tsx +++ b/packages/react-db/tests/conformance.test.tsx @@ -155,11 +155,15 @@ function mountDisabled() { function mountControllable

( build: (q: any, param: P) => any, initial: P, + opts?: { pool?: PooledLiveQueries }, ): ControllableHandle

{ const hook = renderHook( ({ param }: { param: P }) => - // Param goes in the dependency list so the hook recompiles when it changes. - useLiveQuery((q: any) => build(q, param), [param]), + opts?.pool + ? // The config form derives identity from the query on each render. + useLiveQuery({ query: (q: any) => build(q, param), pool: opts.pool }) + : // Param goes in the dependency list so the hook recompiles when it changes. + useLiveQuery((q: any) => build(q, param), [param]), { initialProps: { param: initial } }, ) const handle = makeHandle(hook) diff --git a/packages/solid-db/tests/conformance.test.tsx b/packages/solid-db/tests/conformance.test.tsx index 1780879cb2..ae9345a62e 100644 --- a/packages/solid-db/tests/conformance.test.tsx +++ b/packages/solid-db/tests/conformance.test.tsx @@ -211,11 +211,18 @@ function mountDisabled() { function mountControllable

( build: (q: any, param: P) => any, initial: P, + opts?: { pool?: PooledLiveQueries }, ): ControllableHandle

{ const [param, setParam] = createSignal

(initial) const { getResult, dispose } = inRoot(() => - // Reading param() inside the query fn makes Solid recompute on change. - useLiveQuery((q: any) => build(q, param())), + opts?.pool + ? // Reading param() in the config accessor makes Solid recompute on change. + useLiveQuery(() => { + const value = param() + return { query: (q: any) => build(q, value), pool: opts.pool } + }) + : // Reading param() inside the query fn makes Solid recompute on change. + useLiveQuery((q: any) => build(q, param())), ) const handle = makeHandle(getResult, dispose) return { diff --git a/packages/svelte-db/tests/conformance.svelte.test.ts b/packages/svelte-db/tests/conformance.svelte.test.ts index 576a9965a6..28f5e57cf3 100644 --- a/packages/svelte-db/tests/conformance.svelte.test.ts +++ b/packages/svelte-db/tests/conformance.svelte.test.ts @@ -202,11 +202,17 @@ function mountDisabled() { function mountControllable

( build: (q: any, param: P) => any, initial: P, + opts?: { pool?: PooledLiveQueries }, ): ControllableHandle

{ let param = $state(initial) let query: any const dispose = $effect.root(() => { - query = useLiveQuery((q: any) => build(q, param), [() => param]) + query = opts?.pool + ? useLiveQuery( + { query: (q: any) => build(q, param), pool: opts.pool } as any, + [() => param], + ) + : useLiveQuery((q: any) => build(q, param), [() => param]) }) const handle = makeHandle(() => query, dispose) return { diff --git a/packages/vue-db/tests/conformance.test.ts b/packages/vue-db/tests/conformance.test.ts index c35e4cb90b..19c8d670ba 100644 --- a/packages/vue-db/tests/conformance.test.ts +++ b/packages/vue-db/tests/conformance.test.ts @@ -210,10 +210,16 @@ function mountDisabled() { function mountControllable

( build: (q: any, param: P) => any, initial: P, + opts?: { pool?: PooledLiveQueries }, ): ControllableHandle

{ const param = ref(initial) as { value: P } const { result, scope } = runInScope(() => - useLiveQuery((q: any) => build(q, param.value), [() => param.value]), + opts?.pool + ? useLiveQuery( + { query: (q: any) => build(q, param.value), pool: opts.pool } as any, + [() => param.value], + ) + : useLiveQuery((q: any) => build(q, param.value), [() => param.value]), ) const handle = makeHandle(result, scope) return { From e9e5d9c207725919b929cec53517ec5d5136f94e Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 23 Oct 2026 10:31:37 -0600 Subject: [PATCH 20/32] fix: map package subpath exports to source in the doc example check CI does not build the packages before lint. A subpath import such as `@tanstack/db-sqlite-persistence-core/broadcast-coordinator` resolved to dist/ locally and to nothing in CI, so one skill example failed only in CI. Each export in a package's manifest now maps to its source file. Co-authored-by: Isaac --- scripts/check-doc-snippets.mjs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/scripts/check-doc-snippets.mjs b/scripts/check-doc-snippets.mjs index 90dd7e432e..ebbf243094 100644 --- a/scripts/check-doc-snippets.mjs +++ b/scripts/check-doc-snippets.mjs @@ -80,21 +80,28 @@ const markdown = [ ), ] -// Map each workspace package to its source entry. +// Map each workspace package and its subpath exports to source. CI does not +// build the packages, so an export that falls through to dist/ is missing. const paths = {} for (const pkg of readdirSync(join(root, `packages`))) { const manifest = join(root, `packages`, pkg, `package.json`) if (!existsSync(manifest)) continue - const { name, private: unpublished } = JSON.parse( - readFileSync(manifest, `utf8`), - ) + const { + name, + private: unpublished, + exports = {}, + } = JSON.parse(readFileSync(manifest, `utf8`)) // Examples document published packages only. if (unpublished) continue - for (const entry of [`src/index.ts`, `src/index.tsx`]) { - if (existsSync(join(root, `packages`, pkg, entry))) { - paths[name] = [join(`packages`, pkg, entry)] - break - } + for (const subpath of Object.keys(exports)) { + if (subpath === `./package.json`) continue + // `.` is src/index; `./x` is src/x.ts or src/x/index.ts. + const stem = subpath === `.` ? `index` : subpath.slice(2) + const entry = [`${stem}.ts`, `${stem}.tsx`, `${stem}/index.ts`] + .map((file) => join(`packages`, pkg, `src`, file)) + .find((file) => existsSync(join(root, file))) + if (!entry) throw new Error(`No source for ${name} export ${subpath}`) + paths[subpath === `.` ? name : `${name}/${stem}`] = [entry] } } From 141b7bde9684bc272e98aab3f188c7d7b423d0e4 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 23 Oct 2026 11:39:01 -0600 Subject: [PATCH 21/32] fix: address the xhigh review of opt-in operators - groupKeys() compiles without noImplicitAny, as join operators do. Both use one OperatorCallback type. The TypeScript-modes test now covers groupKeys and a reusable join operator, which passes its context explicitly and stays typed in both modes. - Document reusable join operators in the guide and the db-core skill. - Migrate the React Native shopping-list example to include() and the projects README to the current join API. - The changeset and skill say that the join methods are removed. They do not throw the coded error. The changeset notes the new type of resolveLiveQueryValue's pool option. - The bundle test runs every operator value alone and every combination of operator families, and checks group-by.ts exactly. - The doc example check reports typos of imported TanStack names and examples that do not parse, and keeps parentheses in its fingerprint. - Correct ARCHITECTURE.md and includes-runtime.ts on which module owns public-key reduction. - Remove dead code: containsAggregate, the pool key check in poolConfig, the joined-key parameter of materializeCompilation, and two needless exports. - Record the open load-subset replay counterexample (seed 1593300187), which fails the same way on main, in the coverage map. Co-authored-by: Isaac --- .changeset/opt-in-operators.md | 4 +- docs/contributing/oracle-coverage.md | 25 +- docs/guides/live-queries.md | 24 + .../src/components/ListsScreen.tsx | 48 +- examples/react/projects/README.md | 41 +- .../db/skills/db-core/live-queries/SKILL.md | 16 +- packages/db/src/live-query-options.ts | 4 +- packages/db/src/query/builder/group-keys.ts | 6 +- .../db/src/query/builder/join-operators.ts | 19 +- packages/db/src/query/builder/types.ts | 11 + packages/db/src/query/compiler/group-by.ts | 41 -- packages/db/src/query/live/ARCHITECTURE.md | 10 +- .../query/live/collection-config-builder.ts | 8 +- .../db/src/query/live/includes-runtime.ts | 4 +- .../src/query/live/materialized-pipeline.ts | 6 +- .../db/src/query/live/public-key-reduction.ts | 2 +- packages/db/src/query/optimizer.ts | 2 +- ...joins.ts => loose-typescript-operators.ts} | 25 +- .../db/tests/join-typescript-modes.test.ts | 70 --- .../tests/operator-typescript-modes.test.ts | 102 ++++ .../db/tests/opt-in-operator-bundle.test.ts | 90 +++- scripts/check-doc-snippets.mjs | 76 ++- scripts/doc-snippets-baseline.json | 496 ++++++++++-------- 23 files changed, 690 insertions(+), 440 deletions(-) rename packages/db/tests/fixtures/{loose-typescript-joins.ts => loose-typescript-operators.ts} (50%) delete mode 100644 packages/db/tests/join-typescript-modes.test.ts create mode 100644 packages/db/tests/operator-typescript-modes.test.ts diff --git a/.changeset/opt-in-operators.md b/.changeset/opt-in-operators.md index 7ab3b2ccd4..c6eecf8085 100644 --- a/.changeset/opt-in-operators.md +++ b/.changeset/opt-in-operators.md @@ -9,8 +9,8 @@ Breaking: joins, grouping, includes, and pooling are now opt-in. An app bundles an operator's code only when it imports the value for that operator. A typical app is about 10 kB smaller after gzip. This app has a query collection, a filtered live query, and React `useLiveQuery`. -- Joins take a join operator. Change `.join(source, on)` to `.join(leftJoin(source, on))`, and `.innerJoin(source, on)` to `.join(innerJoin(source, on))`. `rightJoin`, `fullJoin`, and `joinOf(type, source, on)` work the same way. The old call shape throws `JoinOperatorRequiredError`. +- Joins take a join operator. Change `.join(source, on)` to `.join(leftJoin(source, on))`, and `.innerJoin(source, on)` to `.join(innerJoin(source, on))`. `rightJoin`, `fullJoin`, and `joinOf(type, source, on)` work the same way. The `.join(source, on)` call shape throws `JoinOperatorRequiredError`. This release removes the `.leftJoin()`, `.innerJoin()`, `.rightJoin()`, and `.fullJoin()` methods. - An include needs `include()`, `toArray()`, or `materialize()`. Change `issues: q.from(...)` in `select()` to `issues: include(q.from(...))`. A subquery in `select()` without one of these throws `IncludeOperatorRequiredError`. - Aggregates such as `count()` need no change. A `groupBy` with no aggregate takes `groupKeys()`: `.groupBy(groupKeys(({ todo }) => todo.status))`. Without it, the query throws `GroupingOperatorRequiredError`. -- Hooks do not pool live queries by default. To share one partition among many equality-filtered queries, pass `pool: pooledLiveQueries` in the hook's query config: `useLiveQuery({ query, pool: pooledLiveQueries })`. Results do not change. Angular's `{ params, query }` form takes `pool` the same way. +- Hooks do not pool live queries by default. To share one partition among many equality-filtered queries, pass `pool: pooledLiveQueries` in the hook's query config: `useLiveQuery({ query, pool: pooledLiveQueries })`. Results do not change. Angular's `{ params, query }` form takes `pool` the same way. For adapter authors: the `pool` option of `resolveLiveQueryValue` is now `pooledLiveQueries` or `false`, not a boolean. - Code that builds query IR by hand must supply the operator code. `IR.Aggregate` takes the grouping compiler as its third argument. `IR.IncludesSubquery` takes the include runtime as its fifth argument. `IR.JoinClause` has a required `runtime` field. Queries that the query builder builds need no change. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index aabdad3e61..0766f8b4d7 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -3773,11 +3773,14 @@ Joins, grouping, includes, and pooling ship only with the value that names them: a join operator, an aggregate or `groupKeys`, an include function, or `pooledLiveQueries`. `packages/db/tests/opt-in-operator-bundle.test.ts` owns that law. It bundles small apps from source with esbuild and reads which -modules reach the output: an app that imports none keeps every operator out, -and each operator's value brings its modules in without the others. The -positive controls keep a renamed or moved module from passing the absence -checks. A mutant that statically imports `BucketFacadeAdapter` into the -live-query builder fails the absence checks. +modules reach the output. Each of the 15 operator values runs alone, and every +one of the 16 combinations of operator families runs with rotating values. An +operator's modules must be present exactly when the app imports one of its +values, and public-key reduction exactly when it imports a join or an include. +The single-value runs are positive controls, so a renamed or moved module +cannot pass the absence checks. A mutant that statically imports +`BucketFacadeAdapter` into the live-query builder, and one that uses a +`group-by.ts` helper from the live-query Collection builder, fail it. The owner checks module reachability only, not byte budgets; `pnpm size:compare` measures bytes. It bundles `@tanstack/db` alone, with other @@ -3785,3 +3788,15 @@ packages external, so it does not check db-ivm operators or framework adapters. Query results under the new API are owned by the existing query and includes oracles, which run with migrated queries; no oracle compares a query's old and new spellings on one input. + +Open counterexample, not fixed by this change: the ordered-window random +campaign in +`packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts` fails +with `TANSTACK_DB_ORACLE_SEED=1593300187` (path `121:1:2:3:2:2`). The +history is `hold`, `truncateMarkers`, `settle` with a load failure, over an +ascending limit-one window on an on-demand marker source with includes. The +convergence check sees a terminal error. Main at `65d9ada50` fails with the +same counterexample, so the operator change did not introduce it. Owner: the +load-subset replay owner. Needed witness: a decision whether a truncate +during a held, then failed, load may end the window in a terminal error, and +the fix or model change that follows. diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index f3fe9bebf6..bf081035b6 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -1106,6 +1106,30 @@ const userPostComments = createLiveQueryCollection((q) => ) ``` +### Reusable Join Operators + +A join operator is a value, so you can build one once and use it in many queries. Inside `.join()`, the operator gets the query's sources from the query. An operator built outside `.join()` does not see the query, so give it the query's context explicitly. Otherwise its condition is untyped, and a wrong field name compiles without an error: + +```ts +import { createLiveQueryCollection, eq, leftJoin } from '@tanstack/db' +import type { ContextFromSource } from '@tanstack/db' + +// The sources that queries using this operator start from. +type UserQuery = ContextFromSource<{ user: typeof usersCollection }> + +export const withPosts = leftJoin<{ post: typeof postsCollection }, UserQuery>( + { post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId), +) + +const usersWithPosts = createLiveQueryCollection((q) => + q + .from({ user: usersCollection }) + .join(withPosts) + .select(({ user, post }) => ({ name: user.name, title: post.title })), +) +``` + ## Subqueries Subqueries allow you to use the result of one query as input to another, they are embedded within the query itself and are compile to a single query pipeline. They are very similar to SQL subqueries that are executed as part of a single operation. diff --git a/examples/react-native/shopping-list/src/components/ListsScreen.tsx b/examples/react-native/shopping-list/src/components/ListsScreen.tsx index d46b277644..bd8c4dcf14 100644 --- a/examples/react-native/shopping-list/src/components/ListsScreen.tsx +++ b/examples/react-native/shopping-list/src/components/ListsScreen.tsx @@ -10,7 +10,7 @@ import { View, } from 'react-native' import { useRouter } from 'expo-router' -import { count, eq, useLiveQuery } from '@tanstack/react-db' +import { count, eq, include, useLiveQuery } from '@tanstack/react-db' import { itemsCollection, listsCollection } from '../db/collections' import { useShopping } from '../db/ShoppingContext' @@ -116,26 +116,32 @@ export function ListsScreen() { name: list.name, createdAt: list.createdAt, $synced: list.$synced, - totalItems: q - .from({ item: itemsCollection }) - .where(({ item }) => eq(item.listId, list.id)) - .select(({ item }) => ({ n: count(item.id) })), - uncheckedPreview: q - .from({ item: itemsCollection }) - .where(({ item }) => eq(item.listId, list.id)) - .where(({ item }) => eq(item.checked, false)) - .select(({ item }) => ({ - id: item.id, - text: item.text, - createdAt: item.createdAt, - })) - .orderBy(({ item }) => item.createdAt, `asc`) - .limit(3), - checkedItems: q - .from({ item: itemsCollection }) - .where(({ item }) => eq(item.listId, list.id)) - .where(({ item }) => eq(item.checked, true)) - .select(({ item }) => ({ n: count(item.id) })), + totalItems: include( + q + .from({ item: itemsCollection }) + .where(({ item }) => eq(item.listId, list.id)) + .select(({ item }) => ({ n: count(item.id) })), + ), + uncheckedPreview: include( + q + .from({ item: itemsCollection }) + .where(({ item }) => eq(item.listId, list.id)) + .where(({ item }) => eq(item.checked, false)) + .select(({ item }) => ({ + id: item.id, + text: item.text, + createdAt: item.createdAt, + })) + .orderBy(({ item }) => item.createdAt, `asc`) + .limit(3), + ), + checkedItems: include( + q + .from({ item: itemsCollection }) + .where(({ item }) => eq(item.listId, list.id)) + .where(({ item }) => eq(item.checked, true)) + .select(({ item }) => ({ n: count(item.id) })), + ), })) .orderBy(({ list }) => list.createdAt, `desc`), }) diff --git a/examples/react/projects/README.md b/examples/react/projects/README.md index 502a2aa961..05cbfda5df 100644 --- a/examples/react/projects/README.md +++ b/examples/react/projects/README.md @@ -78,7 +78,7 @@ Now that you have two routes you can use a `Link` component to navigate between To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. ```tsx -import { Link } from '@tanstack/react-router' +import { Link } from "@tanstack/react-router" ``` Then anywhere in your JSX you can use it like so: @@ -98,10 +98,10 @@ In the File Based Routing setup the layout is located in `src/routes/__root.tsx` Here is an example layout that includes a header: ```tsx -import { Outlet, createRootRoute } from '@tanstack/react-router' -import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' +import { Outlet, createRootRoute } from "@tanstack/react-router" +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" -import { Link } from '@tanstack/react-router' +import { Link } from "@tanstack/react-router" export const Route = createRootRoute({ component: () => ( @@ -132,9 +132,9 @@ For example: ```tsx const peopleRoute = createRoute({ getParentRoute: () => rootRoute, - path: '/people', + path: "/people", loader: async () => { - const response = await fetch('https://swapi.dev/api/people') + const response = await fetch("https://swapi.dev/api/people") return response.json() as Promise<{ results: { name: string @@ -180,16 +180,16 @@ Built on a TypeScript implementation of differential dataflow, TanStack DB provi This example uses Query Collections for server-state synchronization with tRPC: ```tsx -import { createCollection } from '@tanstack/react-db' -import { queryCollectionOptions } from '@tanstack/query-db-collection' -import { QueryClient } from '@tanstack/query-core' +import { createCollection } from "@tanstack/react-db" +import { queryCollectionOptions } from "@tanstack/query-db-collection" +import { QueryClient } from "@tanstack/query-core" const queryClient = new QueryClient() export const todoCollection = createCollection( queryCollectionOptions({ - id: 'todos', - queryKey: ['todos'], + id: "todos", + queryKey: ["todos"], queryFn: async () => { const todos = await trpc.todos.getAll.query() return todos.map((todo) => ({ @@ -228,7 +228,7 @@ const AddTodo = () => { onClick={() => todoCollection.insert({ id: crypto.randomUUID(), - text: '🔥 Make app faster', + text: "🔥 Make app faster", completed: false, }) } @@ -242,20 +242,21 @@ const AddTodo = () => { Use live queries to read data reactively across collections: ```tsx -import { useLiveQuery } from '@tanstack/react-db' +import { eq, innerJoin, useLiveQuery } from "@tanstack/react-db" const Todos = () => { // Read data using live queries with cross-collection joins const { data: todos } = useLiveQuery((query) => query .from({ t: todoCollection }) - .join({ - type: 'inner', - from: { l: listCollection }, - on: [`@l.id`, `=`, `@t.list_id`], - }) - .where('@l.active', '=', true) - .select('@t.id', '@t.text', '@t.status', '@l.name') + .join(innerJoin({ l: listCollection }, ({ t, l }) => eq(l.id, t.list_id))) + .where(({ l }) => eq(l.active, true)) + .select(({ t, l }) => ({ + id: t.id, + text: t.text, + status: t.status, + name: l.name, + })) ) return ( diff --git a/packages/db/skills/db-core/live-queries/SKILL.md b/packages/db/skills/db-core/live-queries/SKILL.md index b0bd0feb90..9299e4d4df 100644 --- a/packages/db/skills/db-core/live-queries/SKILL.md +++ b/packages/db/skills/db-core/live-queries/SKILL.md @@ -115,7 +115,7 @@ non-null value. ### 2. Joining two collections -Join conditions accept `eq()` or a nonempty, possibly nested `and()` of equalities (equality only -- IVM constraint). Joins take an imported operator: `.join(leftJoin(source, on))`, and likewise `innerJoin`, `rightJoin`, `fullJoin`, and `joinOf(type, source, on)`. The `.join(source, on)` call shape and the `.leftJoin()` methods are removed and throw `JoinOperatorRequiredError`. +Join conditions accept `eq()` or a nonempty, possibly nested `and()` of equalities (equality only -- IVM constraint). Joins take an imported operator: `.join(leftJoin(source, on))`, and likewise `innerJoin`, `rightJoin`, `fullJoin`, and `joinOf(type, source, on)`. The `.join(source, on)` call shape throws `JoinOperatorRequiredError`. The `.leftJoin()`, `.innerJoin()`, `.rightJoin()`, and `.fullJoin()` methods are removed. ```ts import { createLiveQueryCollection, eq, innerJoin } from '@tanstack/db' @@ -151,6 +151,20 @@ q.from({ user: usersCollection }) ) ``` +A reusable operator built outside `.join()` needs the query's context as a type argument. Without it, the condition is untyped: + +```ts +import { eq, leftJoin } from '@tanstack/db' +import type { ContextFromSource } from '@tanstack/db' + +type UserQuery = ContextFromSource<{ user: typeof usersCollection }> +export const withPosts = leftJoin<{ post: typeof postsCollection }, UserQuery>( + { post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId), +) +q.from({ user: usersCollection }).join(withPosts) +``` + ### 3. Aggregation with groupBy + having Use `groupBy` to group rows, then aggregate in `select`. Filter groups with `having`. The `$selected` namespace lets `having` and `orderBy` reference fields defined in `select`. A `groupBy` with no aggregate wraps its keys in `groupKeys()`: `.groupBy(groupKeys(({ order }) => order.status))`; without it the query throws `GroupingOperatorRequiredError`. diff --git a/packages/db/src/live-query-options.ts b/packages/db/src/live-query-options.ts index 1d1e54f5a2..f6d0966c58 100644 --- a/packages/db/src/live-query-options.ts +++ b/packages/db/src/live-query-options.ts @@ -226,9 +226,7 @@ function poolConfig( ): Collection | undefined { if ( !(config.query instanceof BaseQueryBuilder) || - !Object.keys(config).every( - (key) => key === `query` || key === `gcTime` || key === `pool`, - ) + !Object.keys(config).every((key) => key === `query` || key === `gcTime`) ) { return undefined } diff --git a/packages/db/src/query/builder/group-keys.ts b/packages/db/src/query/builder/group-keys.ts index 9f20359b94..f45df0b983 100644 --- a/packages/db/src/query/builder/group-keys.ts +++ b/packages/db/src/query/builder/group-keys.ts @@ -1,6 +1,6 @@ import { grouping } from '../compiler/grouping.js' import type { Grouping } from '../compiler/grouping.js' -import type { Context, GroupByCallback } from './types.js' +import type { Context, GroupByCallback, OperatorCallback } from './types.js' /** * Group keys for `query.groupBy(...)` when the query has no aggregate. @@ -11,8 +11,8 @@ export interface GroupKeys { grouping: Grouping } -export function groupKeys( - callback: GroupByCallback, +export function groupKeys( + callback: OperatorCallback>, ): GroupKeys { return { callback, grouping } } diff --git a/packages/db/src/query/builder/join-operators.ts b/packages/db/src/query/builder/join-operators.ts index be24180b25..91c7acb914 100644 --- a/packages/db/src/query/builder/join-operators.ts +++ b/packages/db/src/query/builder/join-operators.ts @@ -4,6 +4,7 @@ import type { Context, JoinOnCallback, MergeContextForJoinCallback, + OperatorCallback, SchemaFromSource, Source, } from './types.js' @@ -37,18 +38,20 @@ export type JoinOperatorFunction = < ) => JoinOperator /** - * A join condition. `.join()` supplies the query's context; when TypeScript - * runs without noImplicitAny it skips that inference, and the condition is - * then untyped, as other unannotated callbacks are in that mode. + * A join condition. `.join()` supplies the query's context. A reusable + * operator built outside `.join()` passes it explicitly: + * `leftJoin<{ post: typeof posts }, ContextFromSource<{ user: typeof users }>>(...)`. + * Without it, the condition is untyped. */ export type JoinCondition< TContext extends Context, TSource extends Source, -> = 0 extends 1 & TContext - ? (refs: any) => any - : JoinOnCallback< - MergeContextForJoinCallback> - > +> = OperatorCallback< + TContext, + JoinOnCallback< + MergeContextForJoinCallback> + > +> function joinOperator( type: TJoinType, diff --git a/packages/db/src/query/builder/types.ts b/packages/db/src/query/builder/types.ts index b5616539e0..cc04f4f1d7 100644 --- a/packages/db/src/query/builder/types.ts +++ b/packages/db/src/query/builder/types.ts @@ -653,6 +653,17 @@ export type GroupByCallback = ( refs: RefsForContext, ) => any +/** + * A callback that an operator value receives before the query supplies its + * context. Without noImplicitAny, TypeScript skips that inference and the + * context is `any`; the callback is then untyped, as other unannotated + * callbacks are in that mode. + */ +export type OperatorCallback< + TContext extends Context, + TCallback, +> = 0 extends 1 & TContext ? (refs: any) => any : TCallback + /** * JoinOnCallback - Type for join condition callback functions * diff --git a/packages/db/src/query/compiler/group-by.ts b/packages/db/src/query/compiler/group-by.ts index 05ef0aa755..d41bfcfbcf 100644 --- a/packages/db/src/query/compiler/group-by.ts +++ b/packages/db/src/query/compiler/group-by.ts @@ -849,47 +849,6 @@ function evaluateWrappedAggregates( } } -/** - * Checks whether an expression contains an aggregate anywhere in its tree. - * Returns true for a top-level Aggregate, or a Func whose args (recursively) - * contain an Aggregate. Safely returns false for nested Select objects. - */ -export function containsAggregate( - expr: BasicExpression | Aggregate | Select | { type: string }, -): boolean { - if (isConditionalSelect(expr)) { - const branchHasAggregate = expr.branches.some( - (branch) => - containsAggregate(branch.condition) || containsAggregate(branch.value), - ) - - return ( - branchHasAggregate || - (expr.defaultValue !== undefined && containsAggregate(expr.defaultValue)) - ) - } - - if (isNestedSelectObject(expr)) { - return Object.values(expr).some((value) => - containsAggregate(value as BasicExpression | Aggregate | Select), - ) - } - - if (!isExpressionLike(expr)) { - return false - } - - if (expr.type === `agg`) { - return true - } - if (expr.type === `func` && `args` in expr) { - return (expr.args as Array).some( - (arg: BasicExpression | Aggregate) => containsAggregate(arg), - ) - } - return false -} - /** * Walks an expression tree containing nested aggregates. * Each Aggregate node is extracted, assigned a synthetic alias (__agg_N), diff --git a/packages/db/src/query/live/ARCHITECTURE.md b/packages/db/src/query/live/ARCHITECTURE.md index b13a92443c..e37e819956 100644 --- a/packages/db/src/query/live/ARCHITECTURE.md +++ b/packages/db/src/query/live/ARCHITECTURE.md @@ -99,7 +99,8 @@ operators and a few boundary adapters: | Architectural role | Concrete implementation | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | Compile relation IDs and demand plans | `packages/db/src/query/compiler/index.ts`, `packages/db/src/query/compiler/joins.ts` | -| Reduce public keys and build routes | `packages/db/src/query/live/materialized-pipeline.ts` | +| Reduce public keys | `packages/db/src/query/live/public-key-reduction.ts` | +| Build include routes | `packages/db/src/query/live/materialized-pipeline.ts` | | Run the graph and publish root rows | `packages/db/src/query/live/collection-config-builder.ts` | | Schedule Collection and Effect graph turns | `packages/db/src/query/live/graph-scheduler.ts` | | Publish Collection-valued buckets | `packages/db/src/query/live/bucket-facade-adapter.ts` | @@ -109,15 +110,16 @@ operators and a few boundary adapters: Operator values carry their compilers, so an app bundles only the operators it imports. `include()`, `toArray()`, and `materialize()` carry the materializer and the Collection-facade adapter (`includes-runtime.ts`); join operators carry -the join compiler, the optimizer's join pushdown, and the materializer for key -reduction; aggregates and `groupKeys()` carry the grouping compiler. A query +the join compiler, the optimizer's join pushdown, and public-key reduction +(`public-key-reduction.ts`), without the include materializer; aggregates and `groupKeys()` carry the grouping compiler. A query that uses an operator always holds the value that carries it, so compilation stays synchronous. Queries without includes keep the original compiled pipeline and do not pay for facade state. The one exception is a joined query with a custom public-key function: its possible duplicate contributors still pass through the keyed -reduction that enforces public-key congruence and multiplicity. +reduction that enforces public-key congruence and multiplicity. The include +materializer uses the same reduction for its root rows. ### Loading handoffs diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index cb92e505b5..b761527b03 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -910,16 +910,12 @@ export class CollectionConfigBuilder< ) // Includes bring the materializer. A joined query with a custom key - // also reduces its public keys, so its join operator brings it too. + // reduces its public keys, so its join operator brings the reduction. const includesRuntime = compilation.includes?.[0]?.runtime const joinClause = this.findJoinClause(this.query) const getKey = this.config.getKey const materialized = includesRuntime - ? includesRuntime.materializeCompilation( - compilation, - getKey, - joinClause !== undefined, - ) + ? includesRuntime.materializeCompilation(compilation, getKey) : getKey && joinClause ? joinClause.runtime.reducePublicKeys(compilation, getKey) : { pipeline: compilation.pipeline, facades: undefined } diff --git a/packages/db/src/query/live/includes-runtime.ts b/packages/db/src/query/live/includes-runtime.ts index 526a1a0726..c390a157c7 100644 --- a/packages/db/src/query/live/includes-runtime.ts +++ b/packages/db/src/query/live/includes-runtime.ts @@ -3,8 +3,8 @@ import { BucketFacadeAdapter } from './bucket-facade-adapter.js' /** * The include materializer. `include()`, `toArray()`, and `materialize()` - * carry it onto the query IR, and join operators carry it for joined queries - * with a custom key, so other apps do not bundle it. + * carry it onto the query IR, so other apps do not bundle it. Join operators + * carry only public-key reduction. */ export const includesRuntime = { materializeCompilation, diff --git a/packages/db/src/query/live/materialized-pipeline.ts b/packages/db/src/query/live/materialized-pipeline.ts index e515ecffec..c9d3345d44 100644 --- a/packages/db/src/query/live/materialized-pipeline.ts +++ b/packages/db/src/query/live/materialized-pipeline.ts @@ -83,12 +83,8 @@ let nextBucketFacadeEdgeId = 0 export function materializeCompilation( compilation: CompilationResult, getRootKey?: (row: any) => unknown, - reduceJoinedPublicKeys = false, ): MaterializedRootCompilation { - if ( - !compilation.includes?.length && - !(getRootKey && reduceJoinedPublicKeys) - ) { + if (!compilation.includes?.length) { return { pipeline: compilation.pipeline, facades: undefined } } diff --git a/packages/db/src/query/live/public-key-reduction.ts b/packages/db/src/query/live/public-key-reduction.ts index 5c4ffa332e..8065b9a972 100644 --- a/packages/db/src/query/live/public-key-reduction.ts +++ b/packages/db/src/query/live/public-key-reduction.ts @@ -110,7 +110,7 @@ export function canonicalizeByPublicKey( ) as ResultStream } -export function assertCongruentContributors( +function assertCongruentContributors( left: CanonicalResult, right: CanonicalResult, ): void { diff --git a/packages/db/src/query/optimizer.ts b/packages/db/src/query/optimizer.ts index 7c78cc6031..38e8489b23 100644 --- a/packages/db/src/query/optimizer.ts +++ b/packages/db/src/query/optimizer.ts @@ -364,7 +364,7 @@ export function getNullableJoinSources(query: QueryIR): Set { * @param query - The QueryIR to optimize * @returns A new QueryIR with optimizations applied */ -export function applyRecursiveOptimization(query: QueryIR): QueryIR { +function applyRecursiveOptimization(query: QueryIR): QueryIR { // First, recursively optimize any existing subqueries const subqueriesOptimized = { ...query, diff --git a/packages/db/tests/fixtures/loose-typescript-joins.ts b/packages/db/tests/fixtures/loose-typescript-operators.ts similarity index 50% rename from packages/db/tests/fixtures/loose-typescript-joins.ts rename to packages/db/tests/fixtures/loose-typescript-operators.ts index 030bc9ea00..c009b8286d 100644 --- a/packages/db/tests/fixtures/loose-typescript-joins.ts +++ b/packages/db/tests/fixtures/loose-typescript-operators.ts @@ -1,12 +1,13 @@ -// Compiled by join-typescript-modes.test.ts with noImplicitAny on and off. +// Compiled by operator-typescript-modes.test.ts with noImplicitAny on and off. import { createLiveQueryCollection, eq, + groupKeys, innerJoin, joinOf, leftJoin, } from '../../src/index.js' -import type { Collection } from '../../src/index.js' +import type { Collection, ContextFromSource } from '../../src/index.js' type User = { id: number; name: string } type Post = { id: number; userId: number; title: string } @@ -32,3 +33,23 @@ export const joined = createLiveQueryCollection((q) => ) .select(({ user, post }) => ({ name: user.name, title: post.title })), ) + +export const grouped = createLiveQueryCollection((q) => + q + .from({ user: users }) + .groupBy(groupKeys(({ user }) => user.name)) + .select(({ user }) => ({ name: user.name })), +) + +// A reusable operator built outside `.join()` passes its context explicitly. +type UserQuery = ContextFromSource<{ user: typeof users }> +export const withPosts = leftJoin<{ post: typeof posts }, UserQuery>( + { post: posts }, + ({ user, post }) => eq(user.id, post.userId), +) +export const reused = createLiveQueryCollection((q) => + q + .from({ user: users }) + .join(withPosts) + .select(({ user, post }) => ({ name: user.name, title: post.title })), +) diff --git a/packages/db/tests/join-typescript-modes.test.ts b/packages/db/tests/join-typescript-modes.test.ts deleted file mode 100644 index 251bdbd470..0000000000 --- a/packages/db/tests/join-typescript-modes.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// @vitest-environment node -import { fileURLToPath } from 'node:url' -import ts from 'typescript' -import { describe, expect, it } from 'vitest' - -// A join operator gets the query's context from `.join()` by inference. -// TypeScript skips that inference without noImplicitAny, so a join condition -// must still compile there, untyped like other callbacks in that mode. -// Strict mode must keep it typed. Type tests run in strict mode only, so this -// compiles one fixture in both modes. -const fixture = (name: string) => - fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)) -const config = ts.getParsedCommandLineOfConfigFile( - fileURLToPath(new URL(`../tsconfig.json`, import.meta.url)), - {}, - { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, -)! - -// A source with a deliberate error lives in memory, so the project's own -// typecheck does not see it. -const wrongField = fixture(`strict-join-field-error.ts`) -const wrongFieldSource = `// Compiled by join-typescript-modes.test.ts. Strict mode must reject the -// wrong field; without noImplicitAny the condition is untyped. -import { createLiveQueryCollection, eq, leftJoin } from '../../src/index.js' -import type { Collection } from '../../src/index.js' - -type User = { id: number; name: string } -type Post = { id: number; userId: number } -declare const users: Collection -declare const posts: Collection - -export const joined = createLiveQueryCollection((q) => - q - .from({ user: users }) - .join( - leftJoin({ post: posts }, ({ user, post }) => eq(user.nope, post.userId)), - ), -) -` - -function errors(file: string, noImplicitAny: boolean) { - const options = { ...config.options, noImplicitAny, noEmit: true } - const host = ts.createCompilerHost(options) - const readFile = host.readFile - host.readFile = (path) => - path === wrongField ? wrongFieldSource : readFile(path) - const fileExists = host.fileExists - host.fileExists = (path) => path === wrongField || fileExists(path) - const program = ts.createProgram([file], options, host) - return ts - .getPreEmitDiagnostics(program, program.getSourceFile(file)) - .map((d) => ts.flattenDiagnosticMessageText(d.messageText, ` `)) -} - -describe(`join conditions across TypeScript modes`, () => { - it.each([true, false])(`compile with noImplicitAny %s`, (noImplicitAny) => { - expect(errors(fixture(`loose-typescript-joins.ts`), noImplicitAny)).toEqual( - [], - ) - }) - - // The untyped fallback must not reach strict mode: a wrong field still fails. - it(`keep a wrong field an error in strict mode only`, () => { - const file = wrongField - expect(errors(file, true)).toEqual([ - expect.stringContaining(`Property 'nope' does not exist`), - ]) - expect(errors(file, false)).toEqual([]) - }) -}) diff --git a/packages/db/tests/operator-typescript-modes.test.ts b/packages/db/tests/operator-typescript-modes.test.ts new file mode 100644 index 0000000000..0dabae9295 --- /dev/null +++ b/packages/db/tests/operator-typescript-modes.test.ts @@ -0,0 +1,102 @@ +// @vitest-environment node +import { fileURLToPath } from 'node:url' +import ts from 'typescript' +import { describe, expect, it } from 'vitest' + +// An operator value, such as a join operator or `groupKeys()`, gets the +// query's context from the clause it is passed to, by inference. TypeScript +// skips that inference without noImplicitAny, so the operator's callback must +// still compile there, untyped like other callbacks in that mode. Strict mode +// must keep it typed. A reusable operator that passes its context explicitly +// is typed in both modes. Type tests run in strict mode only, so this compiles +// the fixtures in both modes. +const fixture = (name: string) => + fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)) +const config = ts.getParsedCommandLineOfConfigFile( + fileURLToPath(new URL(`../tsconfig.json`, import.meta.url)), + {}, + { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, +)! + +// Sources with deliberate errors live in memory, so the project's own +// typecheck does not see them. +const header = `import { createLiveQueryCollection, eq, groupKeys, leftJoin } from '../../src/index.js' +import type { Collection, ContextFromSource } from '../../src/index.js' + +type User = { id: number; name: string } +type Post = { id: number; userId: number } +declare const users: Collection +declare const posts: Collection +` +const wrongFields = { + inlineJoin: `${header} +export const joined = createLiveQueryCollection((q) => + q + .from({ user: users }) + .join(leftJoin({ post: posts }, ({ user, post }) => eq(user.nope, post.userId))), +) +`, + groupKeys: `${header} +export const grouped = createLiveQueryCollection((q) => + q.from({ user: users }).groupBy(groupKeys(({ user }) => user.nope)), +) +`, + reusableJoin: `${header} +export const withPosts = leftJoin<{ post: typeof posts }, ContextFromSource<{ user: typeof users }>>( + { post: posts }, + ({ user, post }) => eq(user.nope, post.userId), +) +`, +} +const inMemory = new Map( + Object.entries(wrongFields).map(([name, source]) => [ + fixture(`wrong-field-${name}.ts`), + source, + ]), +) + +function errors(file: string, noImplicitAny: boolean) { + const options = { + ...config.options, + noImplicitAny, + noUnusedLocals: false, + noEmit: true, + } + const host = ts.createCompilerHost(options) + const readFile = host.readFile + host.readFile = (path) => inMemory.get(path) ?? readFile(path) + const fileExists = host.fileExists + host.fileExists = (path) => inMemory.has(path) || fileExists(path) + const program = ts.createProgram([file], options, host) + return ts + .getPreEmitDiagnostics(program, program.getSourceFile(file)) + .map((d) => ts.flattenDiagnosticMessageText(d.messageText, ` `)) +} + +describe(`operator callbacks across TypeScript modes`, () => { + it.each([true, false])(`compile with noImplicitAny %s`, (noImplicitAny) => { + expect( + errors(fixture(`loose-typescript-operators.ts`), noImplicitAny), + ).toEqual([]) + }) + + // The untyped fallback must not reach strict mode: a wrong field still fails. + it.each([`inlineJoin`, `groupKeys`])( + `keep a wrong field an error in strict mode only: %s`, + (name) => { + const file = fixture(`wrong-field-${name}.ts`) + expect(errors(file, true)).toEqual([ + expect.stringContaining(`Property 'nope' does not exist`), + ]) + expect(errors(file, false)).toEqual([]) + }, + ) + + it(`keep a wrong field an error in a reusable operator in both modes`, () => { + const file = fixture(`wrong-field-reusableJoin.ts`) + for (const noImplicitAny of [true, false]) + expect(errors(file, noImplicitAny)).toEqual([ + expect.stringContaining(`Property 'nope' does not exist`), + ]) + }) +}) diff --git a/packages/db/tests/opt-in-operator-bundle.test.ts b/packages/db/tests/opt-in-operator-bundle.test.ts index 979e9bc616..4804d974d4 100644 --- a/packages/db/tests/opt-in-operator-bundle.test.ts +++ b/packages/db/tests/opt-in-operator-bundle.test.ts @@ -15,9 +15,12 @@ import { describe, expect, it } from 'vitest' * this bundles small apps from source as an app's build would and reads which * modules reach the output. * - * Each operator has a positive control: importing its value must bring its - * modules in. Without the controls, a module renamed or moved would make the - * absence checks pass vacuously. + * The check runs every operator value alone, so each one is a positive control + * for its own modules: without them, a module renamed or moved would make the + * absence checks pass vacuously. It also runs every combination of operator + * families, each family named by one of its values in turn, so a module that + * only a combination reaches is checked too. The modules shared by joins and + * includes must arrive exactly when an app imports one of the two. */ const index = fileURLToPath(new URL(`../src/index.ts`, import.meta.url)) @@ -27,7 +30,10 @@ const modules = { `src/query/compiler/join-runtime.ts`, `src/query/optimizer-joins.ts`, ], - grouping: [`src/query/compiler/grouping.ts`], + grouping: [ + `src/query/compiler/grouping.ts`, + `src/query/compiler/group-by.ts`, + ], includes: [ `src/query/live/bucket-facade-adapter.ts`, `src/query/live/includes-runtime.ts`, @@ -35,10 +41,20 @@ const modules = { ], pooling: [`src/query/pooled-live-query.ts`], } as const +type Family = keyof typeof modules + +/** The values that bring each family's code in. */ +const values: Record> = { + joins: [`leftJoin`, `innerJoin`, `rightJoin`, `fullJoin`, `joinOf`], + grouping: [`count`, `sum`, `avg`, `min`, `max`, `groupKeys`], + includes: [`include`, `toArray`, `materialize`], + pooling: [`pooledLiveQueries`], +} // Joins and includes share public-key reduction; an app with neither keeps it // out. const shared = [`src/query/live/public-key-reduction.ts`] +const families = Object.keys(modules) as Array /** Bundles an app that exports `names` and returns its modules' output bytes. */ async function bundledModules(names: Array) { @@ -68,38 +84,58 @@ async function bundledModules(names: Array) { return bytes } +// `concat` and `caseWhen` build select values but bring no operator code. const typicalApp = [ `createLiveQueryCollection`, `eq`, `and`, + `concat`, + `caseWhen`, `createCollection`, ] +/** The law: an operator's modules are in the bundle exactly when it is imported. */ +function expectOperators( + bytes: ReadonlyMap, + imported: ReadonlySet, +) { + for (const family of families) + for (const file of modules[family]) + expect(bytes.has(file), `${family}: ${file}`).toBe(imported.has(family)) + for (const file of shared) + expect(bytes.has(file), file).toBe( + imported.has(`joins`) || imported.has(`includes`), + ) +} + describe(`opt-in operator bundles`, () => { - it(`keeps every operator out of an app that imports none`, async () => { - const bytes = await bundledModules(typicalApp) - for (const files of Object.values(modules)) { - for (const file of files) expect(bytes.has(file), file).toBe(false) - } - for (const file of shared) expect(bytes.has(file), file).toBe(false) - // Aggregate detection stays in core; the grouping compiler does not. - expect(bytes.get(`src/query/compiler/group-by.ts`) ?? 0).toBeLessThan(1000) + it.each( + Object.entries(values).flatMap(([family, names]) => + names.map((name) => [name, family as Family] as const), + ), + )(`brings in only what %s needs (%s)`, async (name, family) => { + expectOperators( + await bundledModules([...typicalApp, name]), + new Set([family]), + ) }) - it.each([ - [`joins`, [`leftJoin`]], - [`grouping`, [`count`]], - [`grouping`, [`groupKeys`]], - [`includes`, [`include`]], - [`includes`, [`toArray`]], - [`pooling`, [`pooledLiveQueries`]], - ] as const)(`brings in %s with %s`, async (operator, names) => { - const bytes = await bundledModules([...typicalApp, ...names]) - for (const file of modules[operator]) - expect(bytes.has(file), file).toBe(true) - for (const [other, files] of Object.entries(modules)) { - if (other === operator) continue - for (const file of files) expect(bytes.has(file), file).toBe(false) - } + // Every subset of families, the empty one included. Subset i names each + // family by its value at position i, so the combinations rotate values. + const subsets = Array.from({ length: 2 ** families.length }, (_, i) => + families.filter((_family, bit) => i & (1 << bit)), + ) + it.each( + subsets.map( + (subset, i) => [subset.join(` + `) || `none`, subset, i] as const, + ), + )(`keeps the law for %s`, async (_label, subset, i) => { + const names = subset.map( + (family) => values[family][i % values[family].length]!, + ) + expectOperators( + await bundledModules([...typicalApp, ...names]), + new Set(subset), + ) }) }) diff --git a/scripts/check-doc-snippets.mjs b/scripts/check-doc-snippets.mjs index ebbf243094..437ceec1b3 100644 --- a/scripts/check-doc-snippets.mjs +++ b/scripts/check-doc-snippets.mjs @@ -9,7 +9,8 @@ // // node scripts/check-doc-snippets.mjs [--verbose] [--update-baseline] // -// Mark a fence ```ts nocheck to skip an example that is not meant to compile. +// An example that does not parse fails too. Mark a fence ```ts nocheck to +// skip an example that is not meant to compile. // // `doc-snippets-baseline.json` lists examples that already failed when the // check began, by file and a hash of their code. Editing one changes its @@ -32,23 +33,29 @@ const baselinePath = join(root, `scripts`, `doc-snippets-baseline.json`) const baseline = existsSync(baselinePath) ? JSON.parse(readFileSync(baselinePath, `utf8`)) : {} -// The tokens of an example, without the punctuation and spacing that +// The tokens of an example, without the spacing, commas, and semicolons that // formatting changes, so a reformatted example keeps its baseline entry. +// Parentheses stay, because moving one changes what the code calls, except +// the ones formatting puts around multi-line JSX. function fingerprint(code) { const scanner = ts.createScanner(ts.ScriptTarget.Latest, true) scanner.setText(code) - const skip = new Set([ - ts.SyntaxKind.CommaToken, - ts.SyntaxKind.SemicolonToken, - ts.SyntaxKind.OpenParenToken, - ts.SyntaxKind.CloseParenToken, - ]) + const skip = new Set([ts.SyntaxKind.CommaToken, ts.SyntaxKind.SemicolonToken]) + const wrapsJsx = [] const tokens = [] for ( let kind = scanner.scan(); kind !== ts.SyntaxKind.EndOfFileToken; kind = scanner.scan() - ) + ) { + if (kind === ts.SyntaxKind.OpenParenToken) { + const jsx = scanner.lookAhead( + () => scanner.scan() === ts.SyntaxKind.LessThanToken, + ) + wrapsJsx.push(jsx) + if (jsx) continue + } + if (kind === ts.SyntaxKind.CloseParenToken && wrapsJsx.pop()) continue if (!skip.has(kind)) tokens.push( kind === ts.SyntaxKind.StringLiteral @@ -56,6 +63,7 @@ function fingerprint(code) { : // Formatting also reflows the HTML in a component template. scanner.getTokenText().replace(/\s+/g, ``), ) + } return tokens.join(` `) } const hashOf = (code) => @@ -212,6 +220,36 @@ function packageFor(name, file) { const importsTanStack = (code) => /from ['"]@tanstack\//.test(code) +const tanstackImports = (code) => + [ + ...code.matchAll( + /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]@tanstack\//g, + ), + ] + .flatMap((match) => match[1].split(`,`)) + .map((name) => + name + .trim() + .split(/\s+as\s+/) + .pop(), + ) + .filter(Boolean) + +function editDistance(a, b) { + let previous = Array.from({ length: b.length + 1 }, (_, j) => j) + for (let i = 1; i <= a.length; i++) { + const row = [i] + for (let j = 1; j <= b.length; j++) + row[j] = Math.min( + previous[j] + 1, + row[j - 1] + 1, + previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1), + ) + previous = row + } + return previous[b.length] +} + function sourceOf(snippet, declared, autoImported) { const byPackage = new Map() for (const name of autoImported) { @@ -237,6 +275,7 @@ function sourceOf(snippet, declared, autoImported) { const declared = snippets.map(() => new Set()) const unimported = snippets.map(() => new Set()) const autoImported = snippets.map(() => new Set()) +const misspelled = snippets.map(() => new Map()) const unknownName = new Set([2304, 2552, 18004, 2749, 2693]) // Docs often show alternatives side by side in one block, and leave // callback parameters unannotated where an app would annotate them. @@ -276,6 +315,14 @@ for (let pass = 0; pass < 6; pass++) { .flattenDiagnosticMessageText(diagnostic.messageText, ``) .match(/'([^']+)'/)?.[1] if (!name) continue + // A near miss of a TanStack name the example imports is a typo, not app + // code. TypeScript stops suggesting spellings after a few, so compare here. + const suggestion = tanstackImports(snippets[index].code).find( + (imported) => + name.length >= 4 && + editDistance(name, imported) <= (name.length >= 6 ? 2 : 1), + ) + if (suggestion !== undefined) misspelled[index].set(name, suggestion) if (exported.has(name)) { if (importsTanStack(snippets[index].code)) unimported[index].add(name) else if (!autoImported[index].has(name)) { @@ -302,9 +349,14 @@ snippets.forEach((snippet, i) => { const ownDiagnostics = diagnostics.filter( (d) => d.file?.fileName === snippet.name, ) - // Pseudo-code, such as `...` placeholders, is not meant to compile. + // An example that does not parse cannot be checked. Pseudo-code, such as + // `...` placeholders, is marked nocheck. if (ownDiagnostics.some((d) => d.code >= 1000 && d.code < 2000)) { unparsable++ + failures.push({ + snippet, + text: `${snippet.file}:${snippet.line}\n does not parse; mark the fence nocheck if it is pseudo-code`, + }) return } const offset = @@ -319,6 +371,8 @@ snippets.forEach((snippet, i) => { }) for (const name of unimported[i]) messages.push(` uses \`${name}\` without importing it`) + for (const [name, suggestion] of misspelled[i]) + messages.push(` uses \`${name}\`; did you mean \`${suggestion}\`?`) if (messages.length) failures.push({ snippet, @@ -362,7 +416,7 @@ if (updateBaseline && !process.env.SNIPPET_ONLY) { `\n${stale.length} baseline entries now pass or no longer exist; run with --update-baseline:\n${stale.join(`\n`)}`, ) console.log( - `\n${snippets.length} examples in ${markdown.length} files: ${newFailures.length} new failures, ${failures.length - newFailures.length} known, ${unparsable} skipped as pseudo-code`, + `\n${snippets.length} examples in ${markdown.length} files: ${newFailures.length} new failures, ${failures.length - newFailures.length} known, ${unparsable} of them do not parse`, ) process.exitCode = newFailures.length || (stale.length && !process.env.SNIPPET_ONLY) ? 1 : 0 diff --git a/scripts/doc-snippets-baseline.json b/scripts/doc-snippets-baseline.json index d2b15d82fc..1cc124be6e 100644 --- a/scripts/doc-snippets-baseline.json +++ b/scripts/doc-snippets-baseline.json @@ -1,273 +1,355 @@ { "docs/collections/electric-collection.md": [ - "56d9efac4ed9aa8d", - "7422149f2dff594a", - "b16254176b96780a" + "0486fc217d708c59", + "057df6c2a83cf889", + "470d85c1df6bfb3b" ], - "docs/collections/indexed-db-collection.md": ["02840970e37fce46"], + "docs/collections/indexed-db-collection.md": ["3476cde5533b19b2"], "docs/collections/local-only-collection.md": [ - "1616bfc9a1c371dd", - "23aedda50d1a9c4d", - "9b19823410809fa7", - "ab45c1f424a4f5c2", - "fc371ec14b35d52c" + "13e46e2252f4dbbd", + "4f48676d11d25bb2", + "76a01f95be112370", + "78fb65e449e72c19", + "f5cf3e36e1083ca5" ], "docs/collections/local-storage-collection.md": [ - "000e5e37db6d115b", - "44ba7d96e999cffd", - "517ab246f2d8be78", - "bc535604c77c1fa1", - "c38cafc80222d29c", - "dd2763deb14376a3", - "eabd7079c8e94cee" + "68dfa546940822e3", + "6d70c69f14e146cb", + "854e68f3a853a17c", + "878d8cb792f4d5b0", + "9541f8ca231f593a", + "bdd628dee2f762b6", + "efeb8d60df22d488" ], "docs/collections/powersync-collection.md": [ - "08fdc12554aef6bd", - "0ed5441c16943c10", - "2b5c40bcc4e20ec0", - "a0537dcb94f1fdd8", - "f40497dfb722c9dd" + "1121a1d4fce99f72", + "40628938991a9a5d", + "488b4dfd017a9ae2", + "57311a5fda690b32", + "97f8437a36fd8e90", + "f9b4047e27b30af9" ], "docs/collections/query-collection.md": [ - "0229dd13bd4d1ffd", - "061e910f0c004a30", - "1b699b36eff1d332", - "2574bda356de34f2", - "2b6afbd876364c47", - "393233dab9f06f9f", - "40cb3614551740e2", - "7629a8f334d06ac6", - "7fa66ee20b7bfb43", - "8f2171f6414ed15e", - "958f9a40aa89f03c", - "af597bdfb912efff", - "bd88200750280a4c", - "c70312226e5685fc", - "e45e143986a0a5c5" + "046301fc052a33e2", + "0bfe51c7af30358e", + "2f58a36f43b49cb8", + "44d4c515f4b3ccdc", + "4ef53236f1f0e65a", + "575d9fbcd116df58", + "65e9c98efb8b3e74", + "67d653e721eaee23", + "69e008ce605ab5ad", + "73e07cea70107a8e", + "b279d43e710e0231", + "be99b856c3b63250", + "cb4a976619ee1fa5", + "ee1fe297ad8dfc60", + "ef5473d61472a009" ], "docs/collections/query-initial-placeholder-data-design.md": [ - "149bb533130822d5" + "350c8b56b70a7f8c", + "9f453db0efc0849c" ], "docs/collections/trailbase-collection.md": [ - "6ce5b7286c00a06c", - "a3a582bdac23bba1", - "d87926f88d1893b3", - "f52226428b04d159", - "fe61c9648fce3ad9" + "01cacac8e229cdb7", + "22e211040bd2c125", + "e68d29f94f51a2a9", + "ed34019fe86c376e", + "f269484277e69798" ], "docs/contributing/oracle-reviews/initial-full-source-sync-readiness.md": [ - "e8bffd051d9cc384" + "a066dfd28d064b54" ], - "docs/framework/angular/overview.md": ["608090f6634091b7"], - "docs/framework/react/overview.md": ["4b92baa28023107a", "c454038dbd572c86"], - "docs/framework/solid/overview.md": ["0ed5a4dc482e3487"], + "docs/framework/angular/overview.md": ["117cab3d5f61ed99"], + "docs/framework/react/overview.md": ["3c3bb83b3fc3e775", "6b2825723b921019"], + "docs/framework/solid/overview.md": ["f37063d19785d250"], "docs/guides/collection-options-creator.md": [ - "1e45d0804bc49c6d", - "30721b8dbc34c240", - "30959fbff6b0b9b3", - "8e4594291426b79a", - "94e97da76ab64521", - "98f8d2a6f39e3d08", - "a56d2489cdbbbe2d", - "ad2c8cf0431a5130", - "d04164c069c046e7" + "0925d4e5b8913ef2", + "313ae9ea1d3110ca", + "36a717829fe7a5e1", + "3b1440fd434de376", + "3bd638a471fd5b59", + "755c8c3a418b6e57", + "93ffd67d77e6ec6b", + "ba190e00efec6d10", + "c5ef470c4dd269b0", + "d0cd80983d56c809", + "f04b165b11c85380", + "fd3c748916093e11" ], "docs/guides/error-handling.md": [ - "0d42062acd306170", - "21c4f07ed33e73d3", - "2e4f4ffd14769f71", - "39b87a75c1a7cddf", - "39d852db0f643223", - "45b7505ca2e2d743", - "51e4e1b10b424521", - "80f1e12fddaac9ad", - "94f75b445e4e77e3", - "a13c70241881d8bb", - "a227bec199f61386", - "b1ed7616c9a09f1e", - "b3b928d3f7fd310e", - "b9b81423f794c4a8", - "d1c64f9173ac52a0" + "07d89ac059d8301e", + "370b5a889a0c657e", + "408965ed1f1a1d8c", + "434f3e16b5667065", + "480d36ada75cf76f", + "49e974d3e6da59fa", + "4d00c3862124af96", + "5fb95ca2f251fac8", + "7764df8c22397256", + "91f283df2f049bf2", + "92a482bfcf052294", + "c145addddfbc0910", + "d433db5572dee8bc", + "e8b3895ecea7adeb", + "f4a3fbd94630c5ff" ], "docs/guides/live-queries.md": [ - "012683b68aea9bd9", - "1c901a9aab4c3605", - "328e6118b6859a6d", - "350c201eb314ac4a", - "3606a1edf6576b6b", - "47c25de745935ecf", - "49792941ddf68946", - "49c11d80e5db559e", - "4d22db9980cce9ba", - "539715bcca864ac7", - "835b83a3af148c5d", - "8590ef81b4186488", - "9d9f28d1f7ce5e67", - "9e5a76892e694d3f", - "acdeadcf40bb3576", - "c561ebd715ee0dc0", - "d1a56dd2b016d61c", - "ef28fad1f061708d", - "f5dfaaee1c2fc9b1" + "074c637c9ae5e2fd", + "0b9819909945a148", + "0efbf436a9f3c648", + "108bce6e53fb6db3", + "143a5cdb4ed8f489", + "18c8495eb2bb1464", + "1d52eeb4e98cfad9", + "2086357da476bbd9", + "2169e70a40d173c3", + "2c0ec6c4235c48cc", + "30e9733da8adb766", + "34dbeab33d621ae1", + "4090bde66bb6ad0c", + "49546444e94d4039", + "57aa6ee22ed13f63", + "5ff8c938fde7f99b", + "6d54d6e18b1b621b", + "701b097325d06aa2", + "74c346d8d5fefe5d", + "77f5028005f45387", + "7b7a71d57c11728b", + "7d3da9ea7698cec6", + "80b9f6f98147657a", + "93b26021a6a149db", + "98e2bf9459f8b8df", + "b1bc21f844133390", + "b2d8e195d137a4cc", + "b91ca65cae412be3", + "bbeb24f3273fa4a9", + "bee5ef646a9eedf9", + "bef58a069d2b56e4", + "c1f92f5703d5cadd", + "c1fe50598119532b", + "cd707520b3f57fbc", + "ce85c47239e15e71", + "deeb9d9bae889b2f", + "e8a965861e04ca9a", + "eb161ae8e10d86bb", + "fe7335a3dc697c32" ], "docs/guides/mutations.md": [ - "10504e85bc44cb36", - "23c224b97339aad2", - "24f926bb7f5e9bf4", - "4c84ab8dcd0c869b", - "5405b8237edb7edc", - "621ca9c1abe615b2", - "676555290fb5f3ec", - "6b6fed7dbce90a71", - "71b81aa657f0792a", - "7272172699ac9d1f", - "763d3f2ff4f82ff8", - "98f2cab14439b918", - "9b556db14527fe01", - "bc7f2418773fce1c", - "c2461550f9d3e496", - "de14bc736e2ed20c", - "e01722dd826edfe8", - "e1e4ae286396b7b5", - "e79a4c90cf8a5761", - "ff2316ae59ea3bd0" + "00d8f7f810a8e0a4", + "0109cd2a38aa38cc", + "242ec191f6caeea9", + "326c499f6a7b08e4", + "38eb9458383a7b0b", + "3f86cfebf9e1a251", + "41ceaf15e590253d", + "58d38717fe824041", + "5f697b6bf6c5680a", + "61e911a15ecf9669", + "65318a6e7e272459", + "66e0bdec74af8c89", + "71774f96957d7e22", + "76fff84f6fff3daa", + "7cdc612725f909ec", + "8bb09f343ca2bd28", + "91f93d315c0bdf9b", + "a82fa3ae4e430eff", + "b13e28d4ae465646", + "c6642c3b2197cc09", + "d51ec6fdd7a989b7", + "df0c9c4a8d8a1923", + "ea691e556ca34438", + "eb45172e61288f30" ], "docs/guides/offline-transactions.md": [ - "53fad67bfc557743", - "cbb84bbc032fe6d9" + "c32585536373469f", + "cc6919d973fd778a", + "da21c418b77bd126" ], "docs/guides/schemas.md": [ - "0f1234ee3709c004", - "175f5a2c0175199c", - "4feb48bc8c2450d8", - "50b51df6b24125cc", - "571063c019efde35", - "5b2a85437b077d70", - "6ff4f1903d62e884", - "9ce8558a3c61a956", - "b30da55c0692c68c", - "caf0b926ff1b6e4d", - "df0dab60bf88a19b", - "ec0faf98a535caac", - "f9cec253d1e29e88" + "0d33795d809fcad2", + "46a2aa9badfca5ff", + "5fd050e3f473e1c7", + "842aa634a6c7a442", + "855be24decf4b8c4", + "ae69119640588d39", + "b90aab7a782d0cb1", + "bb7c080bc732c7e6", + "c0c92e9d0a785f6c", + "c6b1bd9749350b37", + "dc92059f2f8c03ba", + "ea386b7cee453029", + "ecc48e9de460816e" ], "docs/guides/sqlite-persistence.md": [ - "44d938b27e7d8e56", - "6b656ba540d345bd", - "dca01a57fc0a2e04", - "fb16035447c1c5b4" + "8f4c8b42b76b4369", + "a8fc6a170a0d77fa", + "cd9b8ec84c33b4a3", + "f9f87da84682865b" ], "docs/guides/ssr.md": [ - "43ecdc891c511196", - "7d6d492c15650152", - "86955b615a421d2f", - "bb340f403f9813b1", - "c809652214430dfa" + "18d8f28b327187a5", + "4d60512f4b7fc53b", + "7eb97230d0e16a2f", + "8aededc33b526bef", + "8de4af8db9c3bdb2", + "90f07ace2e5b6c52", + "91c23864b366abde", + "a2033602d6547678", + "fa68e19582406306" ], "docs/overview.md": [ - "29c08f39ce288ed8", - "3a15ab6c1b66500c", - "bccbf6aceee45753", - "be08f67a8db26760", - "c31cc7218909bb3f", - "c64c851e1a775b7d" + "09ab0cf2b6f2849e", + "46bd14b0da34a7c8", + "4d8e7e83cab41085", + "6f3def492fb2769c", + "8d5c0db9cf7a67c3", + "a2eacbf57052e097", + "de401f3885b6e130" ], "docs/quick-start.md": [ - "050709d610c65189", - "4b92baa28023107a", - "9a2229c6576082a7", - "b6f8297128236baf" + "6b2825723b921019", + "a4c8e067b361b0e6", + "d68bc05111c9887c", + "e7b73aa6d4a7582f" ], "packages/angular-db/skills/angular-db/SKILL.md": [ - "05e664d90130374e", - "52cc4733bc84ecdc", - "70be2e9d955c38d4", - "a383923cc25453f2", - "d7162b5c2af01089" + "25b7fee2d442d1c3", + "293aa482f1e106dd", + "400cb43144925775", + "6ba33f3e90701d89", + "b165fab2b59a3492" + ], + "packages/db/skills/db-core/collection-setup/references/electric-adapter.md": [ + "3fa80e1f966c9bf0", + "8e100aad30cfc960", + "f812b7f924964dab" ], "packages/db/skills/db-core/collection-setup/references/local-adapters.md": [ - "4efbd8925c757c30", - "626940148a94ad34", - "d9d940252f67e38d", - "f9bb0de5ca97a91d" + "1db77ecbf639e214", + "815689db3c424bed", + "a79babc86533a994", + "e0280f0adaa76fea" ], "packages/db/skills/db-core/collection-setup/references/powersync-adapter.md": [ - "ea56bb18de1e5bc3" + "ad722477b9537355" ], "packages/db/skills/db-core/collection-setup/references/query-adapter.md": [ - "1d3913d423bc7e38", - "3b24ad26456d3e7c", - "591f73320d6029a0", - "5f24bed8eb699df8", - "9c1631a553ea07d2", - "ac10737f5050571f", - "fcf5567666226944" + "0b2436b69c112166", + "2dedb5c23e9643f4", + "40888ffca141797f", + "479c6ced9f096fa8", + "50f8e6a2f4715b8c", + "5aeb33df2e687b80", + "5bdb459949daccef", + "8f8649c63feca704", + "b3cb97ac1f7e658a", + "d73c9ed791d09294" ], "packages/db/skills/db-core/collection-setup/references/trailbase-adapter.md": [ - "e0e8861a744963a2" + "d19456d34f2829fe" ], "packages/db/skills/db-core/collection-setup/SKILL.md": [ - "070f001f9160a30b", - "26751817f0f8e23a", - "591e4f7c004f672d", - "84f362c63e9360cb", - "9eabaf549c35dd7a", - "a8fde4fbfc21276a", - "abba53377897fe5e", - "af80855f8b6db66c", - "c83cd2bf72d16c54", - "cf64631446425ad8", - "ea4b823c98753caa" - ], - "packages/db/skills/db-core/custom-adapter/SKILL.md": ["0946d1e769d92cf7"], + "27a46b0e250e9b8b", + "4678c7153c57c7ad", + "5c4400cd2b85296c", + "6faada42b5fad1b8", + "73e9be29b2eabd99", + "8e4bb054bababe8a", + "9e3871bbbd486b28", + "a0add18c889fe075", + "b7634e508e808aa1", + "c2e40e9ea4d731a6", + "cb8024131c6c6499", + "d585a26612f2c861", + "f9ee1b53da4a2522" + ], + "packages/db/skills/db-core/custom-adapter/SKILL.md": [ + "3fd318e47cdf7d22", + "54abd1ab294711ab", + "fed30cc8fd62cd2c" + ], "packages/db/skills/db-core/live-queries/references/operators.md": [ - "012683b68aea9bd9" + "e8a965861e04ca9a" ], "packages/db/skills/db-core/live-queries/SKILL.md": [ - "069a9d0f7d5744b2", - "70da1f691de74854", - "d808ae7acfd79f4d", - "fcf8dccac11c4ade" + "22ee57e0e1e13df9", + "4df1b806a2a71b3a", + "50c570435d86dffc", + "5269bfd07303c2b7", + "68539db6b241e22d", + "da372dd6d3097ba5", + "de2fd4bdcdd58cde" ], "packages/db/skills/db-core/mutations-optimistic/references/transaction-api.md": [ - "967d90e913132656" + "202d1f01879cfdcf", + "59e7e51408c2f16d", + "69bbc514541301c6", + "7c427d9e0c514ba7", + "bc0e8d35d9ec9adf", + "f8d9be1374eedd6e", + "ffc88e5fa8edf470" ], "packages/db/skills/db-core/mutations-optimistic/SKILL.md": [ - "1dbef3144d02599f", - "549bc78a7c315a5d", - "6b0a0344d218354e", - "8b95067b56d2d9ca", - "a87b1b6b7f312e36" + "0879ec2811ec296b", + "4d0619bec0abf570", + "5fba13ca4647498c", + "b4f9517b78398f1f", + "d06803218b4d2668", + "ed0f4f265b88e861" ], "packages/db/skills/db-core/persistence/SKILL.md": [ - "226a17d946d2644a", - "42ca999e9550faf3", - "4321c008910761e3", - "49cc6207176cfdd0", - "6e51303b6511a747", - "9d1be38d645f5ff3" - ], - "packages/db/skills/meta-framework/SKILL.md": ["29bfd0d0914b4128"], + "055353a57302f9c0", + "550042d801b88b49", + "906e32cc720a102e", + "92eceb5a26f18594", + "a1f5c7155fd17cc2", + "a45a67fb7467d548" + ], + "packages/db/skills/meta-framework/SKILL.md": [ + "2c23ef1c6d153a6b", + "ac9c6ce8040e97ed", + "b05e12d8d2c0fd9a", + "b7b34c72b79b8bc2" + ], "packages/offline-transactions/skills/offline/SKILL.md": [ - "14055dbaded0d439", - "17d601a70493c3b0", - "9a3cd51e9c17d3f9", - "ba3fc8531b556c3e" + "13adc84e6e5acf58", + "193438a16a3ff165", + "28731f276bad0d7e", + "2e9790f605ddaf82", + "4c0fdefbb072c0a3", + "638e6bd061b3111c", + "67d427db1605b614", + "712fb68b2da868e4", + "777865643b0d79dd", + "969583ae5073e122", + "b67e55956a00d1d4", + "e0669f885466006e", + "e3ccf5757e6c74b8", + "edbb94ac2c0e7585", + "f16e2c64121ac4b2" ], "packages/react-db/skills/react-db/SKILL.md": [ - "25034f3ee0fa0a2b", - "2ef25b5bf5bff30f", - "313daeae3a103f55", - "c878afe32da4edee" + "30a5cc1cc6746eab", + "5edaf62175219943", + "91e3b6fe80674fdc", + "9ca257bfb9411532", + "bb6a162d7ecbc818", + "dc77f6fa62cd9451", + "eb2c28e3daffe7db" + ], + "packages/solid-db/skills/solid-db/SKILL.md": [ + "3409ba5b9fc02338", + "f8202d7a8ac5f9bc" ], - "packages/solid-db/skills/solid-db/SKILL.md": ["7860a5e90890c302"], "packages/svelte-db/skills/svelte-db/SKILL.md": [ - "5c9035dd0f4aad97", - "72cc3552d6a4ade1", - "9f5ba57807c8dd03" + "284e2907646f52df", + "3fd7db2e8f984e85", + "7b85c967548e02a1", + "fae1b78a475fc6db" ], "packages/vue-db/skills/vue-db/SKILL.md": [ - "23d62290a7b76578", - "bb0769916fc9762b" + "aca20ba5b3b8d041", + "da44abed79157835" ] } From 5cc975c96d1c3269fa60996d322f54e3b069e760 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:42:12 +0000 Subject: [PATCH 22/32] ci: apply automated fixes --- examples/react/projects/README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/react/projects/README.md b/examples/react/projects/README.md index 05cbfda5df..cc5d09f9cf 100644 --- a/examples/react/projects/README.md +++ b/examples/react/projects/README.md @@ -78,7 +78,7 @@ Now that you have two routes you can use a `Link` component to navigate between To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. ```tsx -import { Link } from "@tanstack/react-router" +import { Link } from '@tanstack/react-router' ``` Then anywhere in your JSX you can use it like so: @@ -98,10 +98,10 @@ In the File Based Routing setup the layout is located in `src/routes/__root.tsx` Here is an example layout that includes a header: ```tsx -import { Outlet, createRootRoute } from "@tanstack/react-router" -import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" +import { Outlet, createRootRoute } from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' -import { Link } from "@tanstack/react-router" +import { Link } from '@tanstack/react-router' export const Route = createRootRoute({ component: () => ( @@ -132,9 +132,9 @@ For example: ```tsx const peopleRoute = createRoute({ getParentRoute: () => rootRoute, - path: "/people", + path: '/people', loader: async () => { - const response = await fetch("https://swapi.dev/api/people") + const response = await fetch('https://swapi.dev/api/people') return response.json() as Promise<{ results: { name: string @@ -180,16 +180,16 @@ Built on a TypeScript implementation of differential dataflow, TanStack DB provi This example uses Query Collections for server-state synchronization with tRPC: ```tsx -import { createCollection } from "@tanstack/react-db" -import { queryCollectionOptions } from "@tanstack/query-db-collection" -import { QueryClient } from "@tanstack/query-core" +import { createCollection } from '@tanstack/react-db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' +import { QueryClient } from '@tanstack/query-core' const queryClient = new QueryClient() export const todoCollection = createCollection( queryCollectionOptions({ - id: "todos", - queryKey: ["todos"], + id: 'todos', + queryKey: ['todos'], queryFn: async () => { const todos = await trpc.todos.getAll.query() return todos.map((todo) => ({ @@ -228,7 +228,7 @@ const AddTodo = () => { onClick={() => todoCollection.insert({ id: crypto.randomUUID(), - text: "🔥 Make app faster", + text: '🔥 Make app faster', completed: false, }) } @@ -242,7 +242,7 @@ const AddTodo = () => { Use live queries to read data reactively across collections: ```tsx -import { eq, innerJoin, useLiveQuery } from "@tanstack/react-db" +import { eq, innerJoin, useLiveQuery } from '@tanstack/react-db' const Todos = () => { // Read data using live queries with cross-collection joins From 1d28d813394fbb2fbaff0573478346bffab3fb76 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 23 Oct 2026 12:48:57 -0600 Subject: [PATCH 23/32] fix: address the medium review of opt-in operators - The TypeScript-modes test covers every operator and form: join and groupKeys, inline, reusable with context, and reusable without context, in both modes, with a stated result per cell. It compiles one program per mode, so it stays fast. - Document the typed reusable groupKeys() form. - Export JoinCondition and OperatorCallback, so a factory can name the condition type. - The doc example check reads examples through the TypeScript syntax tree: JSX text no longer breaks the fingerprint, every import form is found, and only syntax errors count as "does not parse". The typo check ignores case-only and prefix differences. A self-test with positive and negative cases runs before each check. - Regenerate the API reference. - Record the untraced includes-oracle failure, and the second seed of the load-subset counterexample, in the coverage map. Co-authored-by: Isaac --- docs/contributing/oracle-coverage.md | 14 +- docs/guides/live-queries.md | 10 + .../namespaces/IR/classes/IncludesSubquery.md | 8 +- docs/reference/functions/groupKeys.md | 4 +- docs/reference/functions/joinOf.md | 4 +- .../functions/resolveLiveQueryValue.md | 2 +- docs/reference/index.md | 2 + docs/reference/interfaces/JoinOperator.md | 10 +- .../interfaces/LiveQueryCollectionConfig.md | 20 +- .../ApplyJoinOptionalityToMergedSchema.md | 2 +- .../type-aliases/FunctionalHavingRow.md | 2 +- docs/reference/type-aliases/GetResult.md | 2 +- .../reference/type-aliases/InferResultType.md | 2 +- docs/reference/type-aliases/JoinCondition.md | 25 + docs/reference/type-aliases/JoinOnCallback.md | 2 +- docs/reference/type-aliases/JoinType.md | 2 +- .../MergeContextForJoinCallback.md | 2 +- .../type-aliases/MergeContextWithJoinType.md | 2 +- .../type-aliases/OperatorCallback.md | 25 + docs/reference/type-aliases/Prettify.md | 2 +- docs/reference/type-aliases/Ref.md | 2 +- docs/reference/type-aliases/RefsForContext.md | 2 +- docs/reference/type-aliases/WithResult.md | 2 +- docs/reference/variables/fullJoin.md | 2 +- docs/reference/variables/innerJoin.md | 2 +- docs/reference/variables/leftJoin.md | 2 +- docs/reference/variables/rightJoin.md | 2 +- .../db/skills/db-core/live-queries/SKILL.md | 2 +- packages/db/src/query/index.ts | 2 + .../fixtures/loose-typescript-operators.ts | 22 +- .../tests/operator-typescript-modes.test.ts | 121 +++-- scripts/check-doc-snippets.mjs | 85 +-- scripts/doc-snippet-syntax.mjs | 201 +++++++ scripts/doc-snippets-baseline.json | 496 +++++++++--------- 34 files changed, 673 insertions(+), 412 deletions(-) create mode 100644 docs/reference/type-aliases/JoinCondition.md create mode 100644 docs/reference/type-aliases/OperatorCallback.md create mode 100644 scripts/doc-snippet-syntax.mjs diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 0766f8b4d7..1861eadbd7 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -3796,7 +3796,19 @@ with `TANSTACK_DB_ORACLE_SEED=1593300187` (path `121:1:2:3:2:2`). The history is `hold`, `truncateMarkers`, `settle` with a load failure, over an ascending limit-one window on an on-demand marker source with includes. The convergence check sees a terminal error. Main at `65d9ada50` fails with the -same counterexample, so the operator change did not introduce it. Owner: the +same counterexample, so the operator change did not introduce it. A full +suite run on the branch also found it with seed `-1005431615` (path `68:1:2`), +which shrinks to the same history. The campaign reaches this defect in +ordinary runs, so CI can fail on it intermittently until the defect is fixed. Owner: the load-subset replay owner. Needed witness: a decision whether a truncate during a held, then failed, load may end the window in a terminal error, and the fix or model change that follows. + +Open observation without a trace: during this change, one run of the random +campaign in `packages/db/tests/query/includes-oracle.property.test.ts` failed +on the branch. The run did not record its seed or counterexample. Five later +dedicated runs and two full-suite runs on the branch passed, and the campaign +was not run on `main` at that time. Owner: the includes oracle owner. Needed witness: the failing seed and +path from a later run. Until then, this entry cannot tell an operator +regression from an older defect. A later failure of that campaign must record +its seed here before anyone calls it pre-existing. diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index bf081035b6..dd8fc21f70 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -1530,6 +1530,16 @@ const departments = createLiveQueryCollection((q) => ) ``` +Like a [reusable join operator](#reusable-join-operators), `groupKeys()` built outside `.groupBy()` needs the query's context as a type argument. Without it, the callback is untyped: + +```ts +import { groupKeys } from '@tanstack/db' +import type { ContextFromSource } from '@tanstack/db' + +type UserQuery = ContextFromSource<{ user: typeof usersCollection }> +export const byDepartment = groupKeys(({ user }) => user.departmentId) +``` + ### Method Signature ```ts diff --git a/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md b/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md index 21c4001d72..9604322fd8 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md @@ -54,7 +54,7 @@ Defined in: [packages/db/src/query/ir.ts:293](https://github.com/TanStack/db/blo ###### materializeCompilation -(`compilation`, `getRootKey?`, `reduceJoinedPublicKeys`) => `MaterializedRootCompilation` +(`compilation`, `getRootKey?`) => `MaterializedRootCompilation` ##### parentFilters? @@ -201,7 +201,7 @@ createBucketFacades: (...args) => BucketFacadeAdapter; #### materializeCompilation() ```ts -materializeCompilation: (compilation, getRootKey?, reduceJoinedPublicKeys) => MaterializedRootCompilation; +materializeCompilation: (compilation, getRootKey?) => MaterializedRootCompilation; ``` Compiles inline includes into the same D2 graph as their parent relation. @@ -218,10 +218,6 @@ adapter resolves those references after the graph reaches quiescence. (`row`) => `unknown` -###### reduceJoinedPublicKeys? - -`boolean` = `false` - ##### Returns `MaterializedRootCompilation` diff --git a/docs/reference/functions/groupKeys.md b/docs/reference/functions/groupKeys.md index 35e40986a1..cd36b220f8 100644 --- a/docs/reference/functions/groupKeys.md +++ b/docs/reference/functions/groupKeys.md @@ -13,13 +13,13 @@ Defined in: [packages/db/src/query/builder/group-keys.ts:14](https://github.com/ ### TContext -`TContext` *extends* [`Context`](../interfaces/Context.md) +`TContext` *extends* [`Context`](../interfaces/Context.md) = `any` ## Parameters ### callback -[`GroupByCallback`](../type-aliases/GroupByCallback.md)\<`TContext`\> +[`OperatorCallback`](../type-aliases/OperatorCallback.md)\<`TContext`, [`GroupByCallback`](../type-aliases/GroupByCallback.md)\<`TContext`\>\> ## Returns diff --git a/docs/reference/functions/joinOf.md b/docs/reference/functions/joinOf.md index 9c9f0e8975..69f922caf9 100644 --- a/docs/reference/functions/joinOf.md +++ b/docs/reference/functions/joinOf.md @@ -10,7 +10,7 @@ function joinOf( on): JoinOperator; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:75](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L75) +Defined in: [packages/db/src/query/builder/join-operators.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L78) A join whose type is chosen at run time. @@ -40,7 +40,7 @@ A join whose type is chosen at run time. ### on -`JoinCondition`\<`TContext`, `TSource`\> +[`OperatorCallback`](../type-aliases/OperatorCallback.md)\<`TContext`, [`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\>\> ## Returns diff --git a/docs/reference/functions/resolveLiveQueryValue.md b/docs/reference/functions/resolveLiveQueryValue.md index 62d62fbfe5..1cf930ddf2 100644 --- a/docs/reference/functions/resolveLiveQueryValue.md +++ b/docs/reference/functions/resolveLiveQueryValue.md @@ -9,7 +9,7 @@ function resolveLiveQueryValue(value, __namedParameters): | null; ``` -Defined in: [packages/db/src/live-query-options.ts:240](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L240) +Defined in: [packages/db/src/live-query-options.ts:238](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-options.ts#L238) ## Parameters diff --git a/docs/reference/index.md b/docs/reference/index.md index 6fd50ea7e1..28959dd00c 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -255,6 +255,7 @@ title: "@tanstack/db" - [InputRow](type-aliases/InputRow.md) - [InsertMutationFn](type-aliases/InsertMutationFn.md) - [InsertMutationFnParams](type-aliases/InsertMutationFnParams.md) +- [JoinCondition](type-aliases/JoinCondition.md) - [JoinOnCallback](type-aliases/JoinOnCallback.md) - [JoinType](type-aliases/JoinType.md) - [KeyedNamespacedRow](type-aliases/KeyedNamespacedRow.md) @@ -281,6 +282,7 @@ title: "@tanstack/db" - [NonEmptyArray](type-aliases/NonEmptyArray.md) - [NonSingleResult](type-aliases/NonSingleResult.md) - [OperationType](type-aliases/OperationType.md) +- [OperatorCallback](type-aliases/OperatorCallback.md) - [OperatorName](type-aliases/OperatorName.md) - [OptimisticChangeMessage](type-aliases/OptimisticChangeMessage.md) - [OrderByCallback](type-aliases/OrderByCallback.md) diff --git a/docs/reference/interfaces/JoinOperator.md b/docs/reference/interfaces/JoinOperator.md index 9dac574ed2..b9a9fc0596 100644 --- a/docs/reference/interfaces/JoinOperator.md +++ b/docs/reference/interfaces/JoinOperator.md @@ -3,7 +3,7 @@ id: JoinOperator title: JoinOperator --- -Defined in: [packages/db/src/query/builder/join-operators.ts:17](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L17) +Defined in: [packages/db/src/query/builder/join-operators.ts:18](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L18) A join step for `query.join(...)`. Importing a join operator is what brings the join compiler into a bundle. @@ -30,7 +30,7 @@ the join compiler into a bundle. on: JoinOnCallback ? InferCollectionType : TSource[K] extends CollectionOptionsIdentity ? InferCollectionType : TSource[K] extends QueryBuilder ? ResultValue : never }[K] }>>; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:23](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L23) +Defined in: [packages/db/src/query/builder/join-operators.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L24) *** @@ -40,7 +40,7 @@ Defined in: [packages/db/src/query/builder/join-operators.ts:23](https://github. runtime: object; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:27](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L27) +Defined in: [packages/db/src/query/builder/join-operators.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L28) #### compile() @@ -241,7 +241,7 @@ readonly \[[`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpr source: TSource; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L22) +Defined in: [packages/db/src/query/builder/join-operators.ts:23](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L23) *** @@ -251,4 +251,4 @@ Defined in: [packages/db/src/query/builder/join-operators.ts:22](https://github. type: TJoinType; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:26](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L26) +Defined in: [packages/db/src/query/builder/join-operators.ts:27](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L27) diff --git a/docs/reference/interfaces/LiveQueryCollectionConfig.md b/docs/reference/interfaces/LiveQueryCollectionConfig.md index d2574e07a6..47721c3189 100644 --- a/docs/reference/interfaces/LiveQueryCollectionConfig.md +++ b/docs/reference/interfaces/LiveQueryCollectionConfig.md @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/live/types.ts:79](https://github.com/TanStack optional defaultStringCollation: StringCollationConfig; ``` -Defined in: [packages/db/src/query/live/types.ts:136](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L136) +Defined in: [packages/db/src/query/live/types.ts:135](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L135) Optional compare options for string sorting. If provided, these will be used instead of inheriting from the FROM collection. @@ -36,7 +36,7 @@ If provided, these will be used instead of inheriting from the FROM collection. optional gcTime: number; ``` -Defined in: [packages/db/src/query/live/types.ts:125](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L125) +Defined in: [packages/db/src/query/live/types.ts:124](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L124) GC time for the collection @@ -48,7 +48,7 @@ GC time for the collection optional getKey: (item) => string | number; ``` -Defined in: [packages/db/src/query/live/types.ts:103](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L103) +Defined in: [packages/db/src/query/live/types.ts:102](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L102) Function to extract the key from result items If not provided, defaults to using the key from the D2 stream @@ -86,7 +86,7 @@ optional onDelete: | DeleteMutationFn; ``` -Defined in: [packages/db/src/query/live/types.ts:115](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L115) +Defined in: [packages/db/src/query/live/types.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L114) *** @@ -98,7 +98,7 @@ optional onInsert: | InsertMutationFn; ``` -Defined in: [packages/db/src/query/live/types.ts:113](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L113) +Defined in: [packages/db/src/query/live/types.ts:112](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L112) Optional mutation handlers @@ -112,7 +112,7 @@ optional onUpdate: | UpdateMutationFn; ``` -Defined in: [packages/db/src/query/live/types.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L114) +Defined in: [packages/db/src/query/live/types.ts:113](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L113) *** @@ -124,7 +124,7 @@ query: | QueryBuilder & RootObjectResultConstraint; ``` -Defined in: [packages/db/src/query/live/types.ts:93](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L93) +Defined in: [packages/db/src/query/live/types.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L92) Query builder function that defines the live query @@ -136,7 +136,7 @@ Query builder function that defines the live query optional schema: undefined; ``` -Defined in: [packages/db/src/query/live/types.ts:108](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L108) +Defined in: [packages/db/src/query/live/types.ts:107](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L107) Optional schema for validation @@ -148,7 +148,7 @@ Optional schema for validation optional singleResult: true; ``` -Defined in: [packages/db/src/query/live/types.ts:130](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L130) +Defined in: [packages/db/src/query/live/types.ts:129](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L129) If enabled the collection will return a single object instead of an array @@ -160,6 +160,6 @@ If enabled the collection will return a single object instead of an array optional startSync: boolean; ``` -Defined in: [packages/db/src/query/live/types.ts:120](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L120) +Defined in: [packages/db/src/query/live/types.ts:119](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L119) Start sync / the query immediately diff --git a/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md b/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md index be00b93665..dccdbf6237 100644 --- a/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md +++ b/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md @@ -7,7 +7,7 @@ title: ApplyJoinOptionalityToMergedSchema type ApplyJoinOptionalityToMergedSchema = { [K in keyof TExistingSchema]: K extends TFromSourceNames ? TJoinType extends "right" | "full" ? TExistingSchema[K] | undefined : TExistingSchema[K] : TExistingSchema[K] } & { [K in keyof TNewSchema]: TJoinType extends "left" | "full" ? TNewSchema[K] | undefined : TNewSchema[K] }; ``` -Defined in: [packages/db/src/query/builder/types.ts:1064](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1064) +Defined in: [packages/db/src/query/builder/types.ts:1075](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1075) ApplyJoinOptionalityToMergedSchema - Applies optionality rules when merging schemas diff --git a/docs/reference/type-aliases/FunctionalHavingRow.md b/docs/reference/type-aliases/FunctionalHavingRow.md index 566ccfbd84..9fd73458a5 100644 --- a/docs/reference/type-aliases/FunctionalHavingRow.md +++ b/docs/reference/type-aliases/FunctionalHavingRow.md @@ -7,7 +7,7 @@ title: FunctionalHavingRow type FunctionalHavingRow = TContext["schema"] & TContext["hasResult"] extends true ? object : object; ``` -Defined in: [packages/db/src/query/builder/types.ts:689](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L689) +Defined in: [packages/db/src/query/builder/types.ts:700](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L700) FunctionalHavingRow - Type for the row parameter in functional having callbacks diff --git a/docs/reference/type-aliases/GetResult.md b/docs/reference/type-aliases/GetResult.md index 966adb6430..14e0751144 100644 --- a/docs/reference/type-aliases/GetResult.md +++ b/docs/reference/type-aliases/GetResult.md @@ -7,7 +7,7 @@ title: GetResult type GetResult = Prettify>; ``` -Defined in: [packages/db/src/query/builder/types.ts:1198](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1198) +Defined in: [packages/db/src/query/builder/types.ts:1209](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1209) ## Type Parameters diff --git a/docs/reference/type-aliases/InferResultType.md b/docs/reference/type-aliases/InferResultType.md index 44683c1d2e..0da3a0028d 100644 --- a/docs/reference/type-aliases/InferResultType.md +++ b/docs/reference/type-aliases/InferResultType.md @@ -7,7 +7,7 @@ title: InferResultType type InferResultType = TContext extends SingleResult ? GetResult | undefined : GetResult[]; ``` -Defined in: [packages/db/src/query/builder/types.ts:1090](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1090) +Defined in: [packages/db/src/query/builder/types.ts:1101](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1101) Utility type to infer the query result size (single row or an array) diff --git a/docs/reference/type-aliases/JoinCondition.md b/docs/reference/type-aliases/JoinCondition.md new file mode 100644 index 0000000000..2e1cd4d3d6 --- /dev/null +++ b/docs/reference/type-aliases/JoinCondition.md @@ -0,0 +1,25 @@ +--- +id: JoinCondition +title: JoinCondition +--- + +```ts +type JoinCondition = OperatorCallback>>>; +``` + +Defined in: [packages/db/src/query/builder/join-operators.ts:46](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L46) + +A join condition. `.join()` supplies the query's context. A reusable +operator built outside `.join()` passes it explicitly: +`leftJoin<{ post: typeof posts }, ContextFromSource<{ user: typeof users }>>(...)`. +Without it, the condition is untyped. + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) + +### TSource + +`TSource` *extends* [`Source`](Source.md) diff --git a/docs/reference/type-aliases/JoinOnCallback.md b/docs/reference/type-aliases/JoinOnCallback.md index b4980cbef1..63bc4e963b 100644 --- a/docs/reference/type-aliases/JoinOnCallback.md +++ b/docs/reference/type-aliases/JoinOnCallback.md @@ -7,7 +7,7 @@ title: JoinOnCallback type JoinOnCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:668](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L668) +Defined in: [packages/db/src/query/builder/types.ts:679](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L679) JoinOnCallback - Type for join condition callback functions diff --git a/docs/reference/type-aliases/JoinType.md b/docs/reference/type-aliases/JoinType.md index cdd748b79c..9e848bd012 100644 --- a/docs/reference/type-aliases/JoinType.md +++ b/docs/reference/type-aliases/JoinType.md @@ -7,4 +7,4 @@ title: JoinType type JoinType = "inner" | "left" | "right" | "full"; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:11](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L11) +Defined in: [packages/db/src/query/builder/join-operators.ts:12](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L12) diff --git a/docs/reference/type-aliases/MergeContextForJoinCallback.md b/docs/reference/type-aliases/MergeContextForJoinCallback.md index a9598cc9f4..e1177322c8 100644 --- a/docs/reference/type-aliases/MergeContextForJoinCallback.md +++ b/docs/reference/type-aliases/MergeContextForJoinCallback.md @@ -7,7 +7,7 @@ title: MergeContextForJoinCallback type MergeContextForJoinCallback = Omit & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:1355](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1355) +Defined in: [packages/db/src/query/builder/types.ts:1366](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1366) MergeContextForJoinCallback - Special context for join condition callbacks diff --git a/docs/reference/type-aliases/MergeContextWithJoinType.md b/docs/reference/type-aliases/MergeContextWithJoinType.md index 86b1de39a4..3b10361c7b 100644 --- a/docs/reference/type-aliases/MergeContextWithJoinType.md +++ b/docs/reference/type-aliases/MergeContextWithJoinType.md @@ -7,7 +7,7 @@ title: MergeContextWithJoinType type MergeContextWithJoinType = Omit & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:1010](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1010) +Defined in: [packages/db/src/query/builder/types.ts:1021](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1021) MergeContextWithJoinType - Creates a new context after a join operation diff --git a/docs/reference/type-aliases/OperatorCallback.md b/docs/reference/type-aliases/OperatorCallback.md new file mode 100644 index 0000000000..2fdee056d3 --- /dev/null +++ b/docs/reference/type-aliases/OperatorCallback.md @@ -0,0 +1,25 @@ +--- +id: OperatorCallback +title: OperatorCallback +--- + +```ts +type OperatorCallback = 0 extends 1 & TContext ? (refs) => any : TCallback; +``` + +Defined in: [packages/db/src/query/builder/types.ts:662](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L662) + +A callback that an operator value receives before the query supplies its +context. Without noImplicitAny, TypeScript skips that inference and the +context is `any`; the callback is then untyped, as other unannotated +callbacks are in that mode. + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) + +### TCallback + +`TCallback` diff --git a/docs/reference/type-aliases/Prettify.md b/docs/reference/type-aliases/Prettify.md index 9cf9678865..9e287ecf31 100644 --- a/docs/reference/type-aliases/Prettify.md +++ b/docs/reference/type-aliases/Prettify.md @@ -7,7 +7,7 @@ title: Prettify type Prettify = { [K in keyof T]: T[K] } & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:1397](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1397) +Defined in: [packages/db/src/query/builder/types.ts:1408](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1408) Prettify - Utility type for clean IDE display diff --git a/docs/reference/type-aliases/Ref.md b/docs/reference/type-aliases/Ref.md index ab4b12b281..215ea634f3 100644 --- a/docs/reference/type-aliases/Ref.md +++ b/docs/reference/type-aliases/Ref.md @@ -7,7 +7,7 @@ title: Ref type Ref = T extends unknown ? RefBranch : never; ``` -Defined in: [packages/db/src/query/builder/types.ts:926](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L926) +Defined in: [packages/db/src/query/builder/types.ts:937](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L937) Ref - The user-facing ref interface for the query builder diff --git a/docs/reference/type-aliases/RefsForContext.md b/docs/reference/type-aliases/RefsForContext.md index fa220dd355..bbc594376b 100644 --- a/docs/reference/type-aliases/RefsForContext.md +++ b/docs/reference/type-aliases/RefsForContext.md @@ -7,7 +7,7 @@ title: RefsForContext type RefsForContext = { [K in Exclude>, keyof JoinedRefsForContext | keyof BranchUnionResultRefs>]: RefForContextSchemaValue, K>, IsNullableContextKey> } & TContext["hasResult"] extends true ? object : object & BranchUnionResultRefs & JoinedRefsForContext; ``` -Defined in: [packages/db/src/query/builder/types.ts:788](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L788) +Defined in: [packages/db/src/query/builder/types.ts:799](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L799) ## Type Parameters diff --git a/docs/reference/type-aliases/WithResult.md b/docs/reference/type-aliases/WithResult.md index 359e52e864..20efbc013c 100644 --- a/docs/reference/type-aliases/WithResult.md +++ b/docs/reference/type-aliases/WithResult.md @@ -7,7 +7,7 @@ title: WithResult type WithResult = Prettify & Pick & object>; ``` -Defined in: [packages/db/src/query/builder/types.ts:1386](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1386) +Defined in: [packages/db/src/query/builder/types.ts:1397](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1397) WithResult - Updates a context with a new result type after select() diff --git a/docs/reference/variables/fullJoin.md b/docs/reference/variables/fullJoin.md index 19996ab3b7..1372c735b1 100644 --- a/docs/reference/variables/fullJoin.md +++ b/docs/reference/variables/fullJoin.md @@ -7,4 +7,4 @@ title: fullJoin const fullJoin: JoinOperatorFunction<"full">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L72) +Defined in: [packages/db/src/query/builder/join-operators.ts:75](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L75) diff --git a/docs/reference/variables/innerJoin.md b/docs/reference/variables/innerJoin.md index 3d113df267..09c5a8d13b 100644 --- a/docs/reference/variables/innerJoin.md +++ b/docs/reference/variables/innerJoin.md @@ -7,4 +7,4 @@ title: innerJoin const innerJoin: JoinOperatorFunction<"inner">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:71](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L71) +Defined in: [packages/db/src/query/builder/join-operators.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L74) diff --git a/docs/reference/variables/leftJoin.md b/docs/reference/variables/leftJoin.md index ec2b6aebfd..a8b3650be0 100644 --- a/docs/reference/variables/leftJoin.md +++ b/docs/reference/variables/leftJoin.md @@ -7,4 +7,4 @@ title: leftJoin const leftJoin: JoinOperatorFunction<"left">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:69](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L69) +Defined in: [packages/db/src/query/builder/join-operators.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L72) diff --git a/docs/reference/variables/rightJoin.md b/docs/reference/variables/rightJoin.md index 6d31a927b2..83f29faa38 100644 --- a/docs/reference/variables/rightJoin.md +++ b/docs/reference/variables/rightJoin.md @@ -7,4 +7,4 @@ title: rightJoin const rightJoin: JoinOperatorFunction<"right">; ``` -Defined in: [packages/db/src/query/builder/join-operators.ts:70](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L70) +Defined in: [packages/db/src/query/builder/join-operators.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L73) diff --git a/packages/db/skills/db-core/live-queries/SKILL.md b/packages/db/skills/db-core/live-queries/SKILL.md index 9299e4d4df..b4636a3e09 100644 --- a/packages/db/skills/db-core/live-queries/SKILL.md +++ b/packages/db/skills/db-core/live-queries/SKILL.md @@ -167,7 +167,7 @@ q.from({ user: usersCollection }).join(withPosts) ### 3. Aggregation with groupBy + having -Use `groupBy` to group rows, then aggregate in `select`. Filter groups with `having`. The `$selected` namespace lets `having` and `orderBy` reference fields defined in `select`. A `groupBy` with no aggregate wraps its keys in `groupKeys()`: `.groupBy(groupKeys(({ order }) => order.status))`; without it the query throws `GroupingOperatorRequiredError`. +Use `groupBy` to group rows, then aggregate in `select`. Filter groups with `having`. The `$selected` namespace lets `having` and `orderBy` reference fields defined in `select`. A `groupBy` with no aggregate wraps its keys in `groupKeys()`: `.groupBy(groupKeys(({ order }) => order.status))`; without it the query throws `GroupingOperatorRequiredError`. A `groupKeys()` built outside `.groupBy()` takes the context as a type argument, `groupKeys>(...)`, as a reusable join does; without it the callback is untyped. ```ts import { count, sum, gt } from '@tanstack/db' diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index 6b3dde491a..fcaf00861f 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -43,6 +43,7 @@ export { // Grouping without aggregates export { groupKeys, type GroupKeys } from './builder/group-keys.js' +export type { OperatorCallback } from './builder/types.js' // Join operators export { @@ -51,6 +52,7 @@ export { innerJoin, fullJoin, joinOf, + type JoinCondition, type JoinOperator, type JoinType, } from './builder/join-operators.js' diff --git a/packages/db/tests/fixtures/loose-typescript-operators.ts b/packages/db/tests/fixtures/loose-typescript-operators.ts index c009b8286d..6f2e5e2f57 100644 --- a/packages/db/tests/fixtures/loose-typescript-operators.ts +++ b/packages/db/tests/fixtures/loose-typescript-operators.ts @@ -7,7 +7,11 @@ import { joinOf, leftJoin, } from '../../src/index.js' -import type { Collection, ContextFromSource } from '../../src/index.js' +import type { + Collection, + ContextFromSource, + JoinCondition, +} from '../../src/index.js' type User = { id: number; name: string } type Post = { id: number; userId: number; title: string } @@ -53,3 +57,19 @@ export const reused = createLiveQueryCollection((q) => .join(withPosts) .select(({ user, post }) => ({ name: user.name, title: post.title })), ) + +export const statusKeys = groupKeys(({ user }) => user.name) +export const regrouped = createLiveQueryCollection((q) => + q + .from({ user: users }) + .groupBy(statusKeys) + .select(({ user }) => ({ name: user.name })), +) + +// A factory takes a condition of the exported type. +export const withPostsOn = ( + on: JoinCondition, +) => leftJoin<{ post: typeof posts }, UserQuery>({ post: posts }, on) +export const factoryJoin = withPostsOn(({ user, post }) => + eq(user.id, post.userId), +) diff --git a/packages/db/tests/operator-typescript-modes.test.ts b/packages/db/tests/operator-typescript-modes.test.ts index 0dabae9295..4cb933cf24 100644 --- a/packages/db/tests/operator-typescript-modes.test.ts +++ b/packages/db/tests/operator-typescript-modes.test.ts @@ -27,47 +27,82 @@ type User = { id: number; name: string } type Post = { id: number; userId: number } declare const users: Collection declare const posts: Collection +type UserQuery = ContextFromSource<{ user: typeof users }> ` -const wrongFields = { - inlineJoin: `${header} -export const joined = createLiveQueryCollection((q) => - q - .from({ user: users }) - .join(leftJoin({ post: posts }, ({ user, post }) => eq(user.nope, post.userId))), -) -`, - groupKeys: `${header} -export const grouped = createLiveQueryCollection((q) => - q.from({ user: users }).groupBy(groupKeys(({ user }) => user.nope)), -) -`, - reusableJoin: `${header} -export const withPosts = leftJoin<{ post: typeof posts }, ContextFromSource<{ user: typeof users }>>( +type Form = `inline` | `reusable with context` | `reusable without context` +type Operator = `join` | `groupKeys` + +// A wrong field in each operator and form. A reusable operator is built +// outside its clause, so only an explicit context can type it. +const wrongField: Record> = { + join: { + inline: `export const q1 = createLiveQueryCollection((q) => + q.from({ user: users }).join(leftJoin({ post: posts }, ({ user, post }) => eq(user.nope, post.userId))), +)`, + 'reusable with context': `export const op = leftJoin<{ post: typeof posts }, UserQuery>( { post: posts }, ({ user, post }) => eq(user.nope, post.userId), -) -`, +)`, + 'reusable without context': `export const op = leftJoin({ post: posts }, ({ user, post }) => eq(user.nope, post.userId))`, + }, + groupKeys: { + inline: `export const q1 = createLiveQueryCollection((q) => + q.from({ user: users }).groupBy(groupKeys(({ user }) => user.nope)), +)`, + 'reusable with context': `export const keys = groupKeys(({ user }) => user.nope)`, + 'reusable without context': `export const keys = groupKeys(({ user }) => user.nope)`, + }, } + +// Whether each form must report the wrong field, in strict mode and without +// noImplicitAny. Inline operators follow the mode, as other callbacks do. An +// explicit context types a reusable operator in both modes. Without one, +// TypeScript has no context to infer, and the operator is untyped in both: +// a documented limit, which the guide answers with the explicit form. +const reportsWrongField: Record = { + inline: { strict: true, loose: false }, + 'reusable with context': { strict: true, loose: true }, + 'reusable without context': { strict: false, loose: false }, +} + +const cases = (Object.keys(wrongField) as Array).flatMap((operator) => + (Object.keys(reportsWrongField) as Array

).map( + (form) => [operator, form] as const, + ), +) +const fileOf = (operator: Operator, form: Form) => + fixture(`wrong-field-${operator}-${form.replaceAll(` `, `-`)}.ts`) const inMemory = new Map( - Object.entries(wrongFields).map(([name, source]) => [ - fixture(`wrong-field-${name}.ts`), - source, + cases.map(([operator, form]) => [ + fileOf(operator, form), + `${header}${wrongField[operator][form]}\n`, ]), ) +// One program per mode compiles every file; each test reads its own file's +// diagnostics. A program per file would take seconds per test. +const programs = new Map() function errors(file: string, noImplicitAny: boolean) { - const options = { - ...config.options, - noImplicitAny, - noUnusedLocals: false, - noEmit: true, + let program = programs.get(noImplicitAny) + if (!program) { + const options = { + ...config.options, + noImplicitAny, + noUnusedLocals: false, + noEmit: true, + } + const host = ts.createCompilerHost(options) + const readFile = host.readFile + host.readFile = (path) => inMemory.get(path) ?? readFile(path) + const fileExists = host.fileExists + host.fileExists = (path) => inMemory.has(path) || fileExists(path) + program = ts.createProgram( + [fixture(`loose-typescript-operators.ts`), ...inMemory.keys()], + options, + host, + ) + programs.set(noImplicitAny, program) } - const host = ts.createCompilerHost(options) - const readFile = host.readFile - host.readFile = (path) => inMemory.get(path) ?? readFile(path) - const fileExists = host.fileExists - host.fileExists = (path) => inMemory.has(path) || fileExists(path) - const program = ts.createProgram([file], options, host) return ts .getPreEmitDiagnostics(program, program.getSourceFile(file)) .map((d) => ts.flattenDiagnosticMessageText(d.messageText, ` `)) @@ -80,23 +115,11 @@ describe(`operator callbacks across TypeScript modes`, () => { ).toEqual([]) }) - // The untyped fallback must not reach strict mode: a wrong field still fails. - it.each([`inlineJoin`, `groupKeys`])( - `keep a wrong field an error in strict mode only: %s`, - (name) => { - const file = fixture(`wrong-field-${name}.ts`) - expect(errors(file, true)).toEqual([ - expect.stringContaining(`Property 'nope' does not exist`), - ]) - expect(errors(file, false)).toEqual([]) - }, - ) - - it(`keep a wrong field an error in a reusable operator in both modes`, () => { - const file = fixture(`wrong-field-reusableJoin.ts`) - for (const noImplicitAny of [true, false]) - expect(errors(file, noImplicitAny)).toEqual([ - expect.stringContaining(`Property 'nope' does not exist`), - ]) + it.each(cases)(`reports a wrong field per mode: %s, %s`, (operator, form) => { + const file = fileOf(operator, form) + const expected = reportsWrongField[form] + const wrong = [expect.stringContaining(`Property 'nope' does not exist`)] + expect(errors(file, true)).toEqual(expected.strict ? wrong : []) + expect(errors(file, false)).toEqual(expected.loose ? wrong : []) }) }) diff --git a/scripts/check-doc-snippets.mjs b/scripts/check-doc-snippets.mjs index 437ceec1b3..c3dfb6f885 100644 --- a/scripts/check-doc-snippets.mjs +++ b/scripts/check-doc-snippets.mjs @@ -25,6 +25,13 @@ import { join, relative } from 'node:path' import { fileURLToPath } from 'node:url' import prettier from 'prettier' import ts from 'typescript' +import { + fingerprint, + misspelling, + parses, + selfTest, + tanstackImports, +} from './doc-snippet-syntax.mjs' const root = fileURLToPath(new URL(`..`, import.meta.url)) const verbose = process.argv.includes(`--verbose`) @@ -33,39 +40,6 @@ const baselinePath = join(root, `scripts`, `doc-snippets-baseline.json`) const baseline = existsSync(baselinePath) ? JSON.parse(readFileSync(baselinePath, `utf8`)) : {} -// The tokens of an example, without the spacing, commas, and semicolons that -// formatting changes, so a reformatted example keeps its baseline entry. -// Parentheses stay, because moving one changes what the code calls, except -// the ones formatting puts around multi-line JSX. -function fingerprint(code) { - const scanner = ts.createScanner(ts.ScriptTarget.Latest, true) - scanner.setText(code) - const skip = new Set([ts.SyntaxKind.CommaToken, ts.SyntaxKind.SemicolonToken]) - const wrapsJsx = [] - const tokens = [] - for ( - let kind = scanner.scan(); - kind !== ts.SyntaxKind.EndOfFileToken; - kind = scanner.scan() - ) { - if (kind === ts.SyntaxKind.OpenParenToken) { - const jsx = scanner.lookAhead( - () => scanner.scan() === ts.SyntaxKind.LessThanToken, - ) - wrapsJsx.push(jsx) - if (jsx) continue - } - if (kind === ts.SyntaxKind.CloseParenToken && wrapsJsx.pop()) continue - if (!skip.has(kind)) - tokens.push( - kind === ts.SyntaxKind.StringLiteral - ? JSON.stringify(scanner.getTokenValue()) - : // Formatting also reflows the HTML in a component template. - scanner.getTokenText().replace(/\s+/g, ``), - ) - } - return tokens.join(` `) -} const hashOf = (code) => createHash(`sha256`).update(code).digest(`hex`).slice(0, 16) @@ -133,7 +107,12 @@ for (const file of markdown) { } } -for (const snippet of snippets) snippet.hash = hashOf(fingerprint(snippet.code)) +selfTest() +for (const snippet of snippets) { + const jsx = snippet.name.endsWith(`.tsx`) + snippet.hash = hashOf(fingerprint(snippet.code, jsx)) + snippet.imports = tanstackImports(snippet.code, jsx) +} if (process.env.SNIPPET_ONLY) { const keep = snippets.filter( @@ -220,36 +199,6 @@ function packageFor(name, file) { const importsTanStack = (code) => /from ['"]@tanstack\//.test(code) -const tanstackImports = (code) => - [ - ...code.matchAll( - /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]@tanstack\//g, - ), - ] - .flatMap((match) => match[1].split(`,`)) - .map((name) => - name - .trim() - .split(/\s+as\s+/) - .pop(), - ) - .filter(Boolean) - -function editDistance(a, b) { - let previous = Array.from({ length: b.length + 1 }, (_, j) => j) - for (let i = 1; i <= a.length; i++) { - const row = [i] - for (let j = 1; j <= b.length; j++) - row[j] = Math.min( - previous[j] + 1, - row[j - 1] + 1, - previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1), - ) - previous = row - } - return previous[b.length] -} - function sourceOf(snippet, declared, autoImported) { const byPackage = new Map() for (const name of autoImported) { @@ -317,11 +266,7 @@ for (let pass = 0; pass < 6; pass++) { if (!name) continue // A near miss of a TanStack name the example imports is a typo, not app // code. TypeScript stops suggesting spellings after a few, so compare here. - const suggestion = tanstackImports(snippets[index].code).find( - (imported) => - name.length >= 4 && - editDistance(name, imported) <= (name.length >= 6 ? 2 : 1), - ) + const suggestion = misspelling(name, snippets[index].imports) if (suggestion !== undefined) misspelled[index].set(name, suggestion) if (exported.has(name)) { if (importsTanStack(snippets[index].code)) unimported[index].add(name) @@ -351,7 +296,7 @@ snippets.forEach((snippet, i) => { ) // An example that does not parse cannot be checked. Pseudo-code, such as // `...` placeholders, is marked nocheck. - if (ownDiagnostics.some((d) => d.code >= 1000 && d.code < 2000)) { + if (!parses(snippet.code, snippet.name.endsWith(`.tsx`))) { unparsable++ failures.push({ snippet, diff --git a/scripts/doc-snippet-syntax.mjs b/scripts/doc-snippet-syntax.mjs new file mode 100644 index 0000000000..f49ffd5819 --- /dev/null +++ b/scripts/doc-snippet-syntax.mjs @@ -0,0 +1,201 @@ +// Syntax helpers for check-doc-snippets.mjs, with the cases that pin their +// judgments. `selfTest()` runs on every check, so a change that makes a helper +// miss a defect, or report a correct example, fails before any doc is read. +import ts from 'typescript' + +const parse = (code, jsx) => + ts.createSourceFile( + jsx ? `example.tsx` : `example.ts`, + code, + ts.ScriptTarget.Latest, + true, + jsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ) + +const isJsx = (node) => + ts.isJsxElement(node) || + ts.isJsxSelfClosingElement(node) || + ts.isJsxFragment(node) + +/** + * The tokens of an example, without what formatting changes, so a reformatted + * example keeps its baseline entry: spacing, comments, commas, semicolons, + * the parentheses around multi-line JSX, and the parentheses around a single + * arrow parameter. Other parentheses stay, because moving one changes what + * the code calls. The tree, not a token scan, finds JSX: a parenthesis in JSX + * text is text. + */ +export function fingerprint(code, jsx) { + const tokens = [] + const visit = (node) => { + if (ts.isParenthesizedExpression(node) && isJsx(node.expression)) + return visit(node.expression) + const children = node.getChildren() + if (children.length === 0) { + if ( + node.kind === ts.SyntaxKind.CommaToken || + node.kind === ts.SyntaxKind.SemicolonToken || + node.kind === ts.SyntaxKind.EndOfFileToken + ) + return + tokens.push( + ts.isStringLiteral(node) + ? JSON.stringify(node.text) + : // Formatting also reflows JSX text and the HTML in a template. + node.getText().replace(/\s+/g, ``), + ) + return + } + const bareParameter = + ts.isArrowFunction(node) && + node.parameters.length === 1 && + ts.isIdentifier(node.parameters[0].name) && + !node.parameters[0].type + for (const child of children) + if (!( + bareParameter && + (child.kind === ts.SyntaxKind.OpenParenToken || + child.kind === ts.SyntaxKind.CloseParenToken) + )) + visit(child) + } + visit(parse(code, jsx)) + return tokens.join(` `) +} + +/** + * Whether an example parses. Only syntax counts: a semantic error with a 1xxx + * code, such as a missing default export, is still an error to report. + */ +export function parses(code, jsx) { + const { diagnostics = [] } = ts.transpileModule(code, { + reportDiagnostics: true, + fileName: jsx ? `example.tsx` : `example.ts`, + compilerOptions: { jsx: ts.JsxEmit.Preserve }, + }) + return diagnostics.length === 0 +} + +/** The names an example imports from a TanStack package, in every form. */ +export function tanstackImports(code, jsx) { + const names = [] + for (const statement of parse(code, jsx).statements) { + if ( + !ts.isImportDeclaration(statement) || + !ts.isStringLiteral(statement.moduleSpecifier) || + !statement.moduleSpecifier.text.startsWith(`@tanstack/`) + ) + continue + const clause = statement.importClause + if (clause?.name) names.push(clause.name.text) + const bindings = clause?.namedBindings + if (bindings && ts.isNamedImports(bindings)) + names.push(...bindings.elements.map((element) => element.name.text)) + } + return names +} + +function editDistance(a, b) { + let previous = Array.from({ length: b.length + 1 }, (_, j) => j) + for (let i = 1; i <= a.length; i++) { + const row = [i] + for (let j = 1; j <= b.length; j++) + row[j] = Math.min( + previous[j] + 1, + row[j - 1] + 1, + previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1), + ) + previous = row + } + return previous[b.length] +} + +/** + * The imported name that an unknown name misspells, if any. An app's own + * names often differ from an import only in case (`collection` and + * `Collection`) or by a suffix (`counter` and `count`); those are not typos. + */ +export function misspelling(name, imported) { + if (name.length < 5) return undefined + return imported.find( + (candidate) => + candidate.toLowerCase() !== name.toLowerCase() && + !candidate.startsWith(name) && + !name.startsWith(candidate) && + editDistance(name, candidate) <= (name.length >= 8 ? 2 : 1), + ) +} + +export function selfTest() { + const imports = [ + `leftJoin`, + `innerJoin`, + `count`, + `Collection`, + `QueryClient`, + `useLiveQuery`, + `eq`, + ] + const typos = { + lefJoin: `leftJoin`, + innerJion: `innerJoin`, + useLiveQuey: `useLiveQuery`, + collection: undefined, + queryClient: undefined, + counter: undefined, + counts: undefined, + q: undefined, + Colection: `Collection`, + } + for (const [name, expected] of Object.entries(typos)) { + const found = misspelling(name, imports) + if (found !== expected) + throw new Error( + `self-test: misspelling(${name}) is ${found}, not ${expected}`, + ) + } + + for (const [code, expected] of [ + [`import Db from '@tanstack/db'\nDb()`, true], + [`const query = useLiveQuery((q) => q.from({ todo })`, false], + [`q.from(...)`, false], + ]) + if (parses(code, false) !== expected) + throw new Error(`self-test: parses(${code}) is not ${expected}`) + + const imported = tanstackImports( + [ + `import Default, { lefJoin, type Colection } from '@tanstack/db'`, + `import type { QueryClient } from '@tanstack/query-core'`, + `import { notTanStack } from 'other'`, + ].join(`\n`), + ) + if (imported.join() !== `Default,lefJoin,Colection,QueryClient`) + throw new Error(`self-test: tanstackImports found ${imported.join()}`) + + // Pairs that formatting can produce must keep one fingerprint. + const same = [ + [`f(a, b,);`, `f(a, b)`], + [`list.map(x => x.id)`, `list.map((x) => x.id)`], + [`return (

:)

)`, `return

:)

`], + [ + `return (\n
  • {t.text}
  • \n)`, + `return
  • {t.text}
  • `, + ], + [`q.from({ a })\n .join(x) // why`, `q.from({ a }).join(x)`], + ] + for (const [a, b] of same) + if (fingerprint(a, true) !== fingerprint(b, true)) + throw new Error(`self-test: fingerprints differ for ${a} and ${b}`) + + // Edits that change what the code does must change the fingerprint. + const different = [ + [`.join(leftJoin(src, on))`, `.join(leftJoin(src), on)`], + [`f((a, b) => a)`, `f(a, b => a)`], + [`eq(a.id, b.id)`, `eq(a.id, b.userId)`], + [`'a b'`, `'ab'`], + ] + for (const [a, b] of different) + if (fingerprint(a, true) === fingerprint(b, true)) + throw new Error(`self-test: fingerprints match for ${a} and ${b}`) +} diff --git a/scripts/doc-snippets-baseline.json b/scripts/doc-snippets-baseline.json index 1cc124be6e..8e3e1f91a2 100644 --- a/scripts/doc-snippets-baseline.json +++ b/scripts/doc-snippets-baseline.json @@ -1,355 +1,355 @@ { "docs/collections/electric-collection.md": [ - "0486fc217d708c59", - "057df6c2a83cf889", - "470d85c1df6bfb3b" + "371f515f0b173869", + "9418cc88b5429990", + "dc85bd060471e886" ], - "docs/collections/indexed-db-collection.md": ["3476cde5533b19b2"], + "docs/collections/indexed-db-collection.md": ["310e80f53a98504c"], "docs/collections/local-only-collection.md": [ - "13e46e2252f4dbbd", - "4f48676d11d25bb2", - "76a01f95be112370", - "78fb65e449e72c19", - "f5cf3e36e1083ca5" + "07430f1a112f56ed", + "10078067e9e256b1", + "5bed34bf4c1e3022", + "8ede77c76c306aa1", + "a82cdf1fb51c0eed" ], "docs/collections/local-storage-collection.md": [ - "68dfa546940822e3", - "6d70c69f14e146cb", - "854e68f3a853a17c", - "878d8cb792f4d5b0", - "9541f8ca231f593a", - "bdd628dee2f762b6", - "efeb8d60df22d488" + "005b4c8ee52cbf02", + "0b3c11bb47bc3f33", + "2b98418f2a1426ce", + "3aecd91454f157f4", + "b5a217ca4f43a25d", + "c8983fce1b78192d", + "e82be700dcd8b768" ], "docs/collections/powersync-collection.md": [ - "1121a1d4fce99f72", - "40628938991a9a5d", - "488b4dfd017a9ae2", - "57311a5fda690b32", - "97f8437a36fd8e90", - "f9b4047e27b30af9" + "06cfdef522049d0a", + "2169f8866b68808c", + "593fdd8f16e7bb3c", + "93783ab556f126f4", + "b8931b6ae90d1bdd", + "e627244ec232ca77" ], "docs/collections/query-collection.md": [ - "046301fc052a33e2", - "0bfe51c7af30358e", - "2f58a36f43b49cb8", - "44d4c515f4b3ccdc", + "0dfd4182f7c55105", + "1df96b094278c68b", + "451163a685a956fb", "4ef53236f1f0e65a", - "575d9fbcd116df58", - "65e9c98efb8b3e74", - "67d653e721eaee23", - "69e008ce605ab5ad", - "73e07cea70107a8e", - "b279d43e710e0231", - "be99b856c3b63250", - "cb4a976619ee1fa5", - "ee1fe297ad8dfc60", - "ef5473d61472a009" + "54b7421f5428e352", + "5553816d1f70f5fe", + "58b6192f164394f5", + "8af878bb86475f81", + "a17d2412353cde87", + "ade3a379dbea8f04", + "b2da874232b8e1ce", + "c335bd701becd84c", + "c85d033e71fa3391", + "e35984115be53885", + "ec5b61cefc5339bf" ], "docs/collections/query-initial-placeholder-data-design.md": [ - "350c8b56b70a7f8c", - "9f453db0efc0849c" + "4aab899a80ba22aa", + "6ab521638b08c996" ], "docs/collections/trailbase-collection.md": [ - "01cacac8e229cdb7", - "22e211040bd2c125", - "e68d29f94f51a2a9", - "ed34019fe86c376e", - "f269484277e69798" + "4bd8a1df1fa7d70d", + "6b53a5c3015176d0", + "c07997601d4e67a8", + "c85dbf385cf700f7", + "d8104dcfbe716d0c" ], "docs/contributing/oracle-reviews/initial-full-source-sync-readiness.md": [ "a066dfd28d064b54" ], - "docs/framework/angular/overview.md": ["117cab3d5f61ed99"], - "docs/framework/react/overview.md": ["3c3bb83b3fc3e775", "6b2825723b921019"], - "docs/framework/solid/overview.md": ["f37063d19785d250"], + "docs/framework/angular/overview.md": ["0f2ebd3d5d7a7dc5"], + "docs/framework/react/overview.md": ["6f3b1689dbac470f", "d89b6c8f8cab169d"], + "docs/framework/solid/overview.md": ["6df72815a0829f83"], "docs/guides/collection-options-creator.md": [ - "0925d4e5b8913ef2", + "178ba378787144e2", "313ae9ea1d3110ca", - "36a717829fe7a5e1", - "3b1440fd434de376", - "3bd638a471fd5b59", - "755c8c3a418b6e57", + "36ae1a3ce3020496", + "3a5e006c63c69965", + "540fc1ab803f1ed9", + "5e48cf0b1808915a", + "6bbd6eafb3e59fa5", + "6e646d5e755a6cb3", "93ffd67d77e6ec6b", - "ba190e00efec6d10", - "c5ef470c4dd269b0", + "97ce280ee90738e1", "d0cd80983d56c809", - "f04b165b11c85380", - "fd3c748916093e11" + "e2390caca076fe07" ], "docs/guides/error-handling.md": [ - "07d89ac059d8301e", - "370b5a889a0c657e", - "408965ed1f1a1d8c", - "434f3e16b5667065", - "480d36ada75cf76f", - "49e974d3e6da59fa", - "4d00c3862124af96", - "5fb95ca2f251fac8", - "7764df8c22397256", - "91f283df2f049bf2", - "92a482bfcf052294", + "01d178432245c40b", + "2990abc3c0724666", + "31bbbdb312594461", + "340192b0e982afd3", + "5757f47d8f92b20a", + "5c25be1b3c47f6ab", + "60e9363d30c1d637", + "6e410f732e841a2e", + "8f931d6f08c9b84e", + "b7efa26f16d32802", "c145addddfbc0910", - "d433db5572dee8bc", - "e8b3895ecea7adeb", - "f4a3fbd94630c5ff" + "d0546b51f1a87a0f", + "dd135a2d259a83ea", + "e6e50c9c782ecae8", + "f34d50969702c39b" ], "docs/guides/live-queries.md": [ + "02c41fc67e388b3c", "074c637c9ae5e2fd", - "0b9819909945a148", - "0efbf436a9f3c648", - "108bce6e53fb6db3", + "10a9196f07dc48ba", + "141579ea42caabb7", "143a5cdb4ed8f489", + "188affbe131b90fb", "18c8495eb2bb1464", - "1d52eeb4e98cfad9", - "2086357da476bbd9", - "2169e70a40d173c3", - "2c0ec6c4235c48cc", + "21f092c4ba68f8b7", + "298138e5ec4fa3da", "30e9733da8adb766", - "34dbeab33d621ae1", - "4090bde66bb6ad0c", + "3a3007f2bcd023d5", "49546444e94d4039", - "57aa6ee22ed13f63", - "5ff8c938fde7f99b", - "6d54d6e18b1b621b", - "701b097325d06aa2", - "74c346d8d5fefe5d", - "77f5028005f45387", + "5344f4ae3f0a97ac", + "576e7dee2a203c3b", + "6258952467b59d37", + "69491502d69fd49e", + "6ce994ae2334e0f0", "7b7a71d57c11728b", - "7d3da9ea7698cec6", - "80b9f6f98147657a", - "93b26021a6a149db", + "7f72a99dc93b8051", + "8ff5fe45604eaa8f", "98e2bf9459f8b8df", - "b1bc21f844133390", - "b2d8e195d137a4cc", - "b91ca65cae412be3", - "bbeb24f3273fa4a9", + "9a984bcf2b1d9fdb", + "9b2bc781e86d85b1", + "a039dbf46e062521", + "a383434319068fab", + "aaab165b58980f8b", + "ade01970173359a8", + "b4709cef2a5aeb18", "bee5ef646a9eedf9", "bef58a069d2b56e4", - "c1f92f5703d5cadd", - "c1fe50598119532b", - "cd707520b3f57fbc", - "ce85c47239e15e71", + "c03e9d1cb3359f13", + "c362e8719de14415", + "d7152cc617862b5d", + "dd7c413d2ce6c7df", + "de0c570137f40f4d", "deeb9d9bae889b2f", "e8a965861e04ca9a", "eb161ae8e10d86bb", - "fe7335a3dc697c32" + "fd8058964b4589c2" ], "docs/guides/mutations.md": [ - "00d8f7f810a8e0a4", - "0109cd2a38aa38cc", - "242ec191f6caeea9", + "08575483c85ec9f9", + "0a075297e59413dd", + "0fc08a99c28070bd", + "142d5138b0644540", + "186888192750fbd1", + "1c893ecb05cc517b", + "2112ba6dca3a2b7f", + "286ed4244947a1d6", "326c499f6a7b08e4", - "38eb9458383a7b0b", - "3f86cfebf9e1a251", - "41ceaf15e590253d", - "58d38717fe824041", - "5f697b6bf6c5680a", - "61e911a15ecf9669", - "65318a6e7e272459", - "66e0bdec74af8c89", - "71774f96957d7e22", - "76fff84f6fff3daa", - "7cdc612725f909ec", - "8bb09f343ca2bd28", - "91f93d315c0bdf9b", - "a82fa3ae4e430eff", - "b13e28d4ae465646", - "c6642c3b2197cc09", - "d51ec6fdd7a989b7", - "df0c9c4a8d8a1923", - "ea691e556ca34438", - "eb45172e61288f30" + "3b85ef26bf02720d", + "4d3c6115ad64e6bd", + "5f0beb87b4b1e4c8", + "68f4ab6cb8a16b50", + "75c78d57ec633739", + "7657b80606831f3e", + "81a5da4172e08422", + "85ad418b3d539e1a", + "935ef55166f84366", + "a47ffd2bd978da22", + "b23f0b1aeef99a43", + "b49db0f4e1827558", + "ca216ab01a47bf39", + "dc292443f5e037ac", + "e54b4ba952701be6" ], "docs/guides/offline-transactions.md": [ - "c32585536373469f", - "cc6919d973fd778a", - "da21c418b77bd126" + "01aaa26f0b625d5d", + "a8b3627a269a36ef", + "b22ff5481a36d67e" ], "docs/guides/schemas.md": [ - "0d33795d809fcad2", - "46a2aa9badfca5ff", - "5fd050e3f473e1c7", - "842aa634a6c7a442", - "855be24decf4b8c4", - "ae69119640588d39", - "b90aab7a782d0cb1", - "bb7c080bc732c7e6", - "c0c92e9d0a785f6c", - "c6b1bd9749350b37", - "dc92059f2f8c03ba", - "ea386b7cee453029", - "ecc48e9de460816e" + "150239ecfd212f9e", + "261b9806e0c454d5", + "55bb8b531b58cbb8", + "6382c8b2a4da2455", + "69930192bebeaa01", + "830d39851adfb3df", + "847d96498a6b9f02", + "98926f89ebf1079f", + "afd0428f5b385bf3", + "bc4277ad28f41535", + "ce13948c81262d9a", + "ecc48e9de460816e", + "febe0217e0331212" ], "docs/guides/sqlite-persistence.md": [ - "8f4c8b42b76b4369", - "a8fc6a170a0d77fa", + "6049b3d5eea161a5", "cd9b8ec84c33b4a3", - "f9f87da84682865b" + "dca3677993dbb9f8", + "f11b0660229e60d6" ], "docs/guides/ssr.md": [ - "18d8f28b327187a5", - "4d60512f4b7fc53b", - "7eb97230d0e16a2f", - "8aededc33b526bef", - "8de4af8db9c3bdb2", - "90f07ace2e5b6c52", - "91c23864b366abde", - "a2033602d6547678", - "fa68e19582406306" + "0f83196580f5a029", + "4810f1662cdf27e2", + "6ea020d808f1a559", + "73edcd444319eeba", + "8f6f88cb61cbeb10", + "941eff98a53f4391", + "96aa50abc108a7a8", + "c93592b33a464526", + "d6ede888487b1630" ], "docs/overview.md": [ - "09ab0cf2b6f2849e", - "46bd14b0da34a7c8", - "4d8e7e83cab41085", - "6f3def492fb2769c", - "8d5c0db9cf7a67c3", - "a2eacbf57052e097", - "de401f3885b6e130" + "17fdf90b03a150dd", + "24b83a08692aed97", + "40d603e9bbfc91af", + "4e6338d120ce5848", + "84e9b444488d7a6b", + "92c7e96918d4a6f3", + "df1ef46a41d27548" ], "docs/quick-start.md": [ - "6b2825723b921019", - "a4c8e067b361b0e6", - "d68bc05111c9887c", - "e7b73aa6d4a7582f" + "1c2fe1bfdc225f1f", + "4cc404a2f380d516", + "d89b6c8f8cab169d", + "e4de8f646f661d71" ], "packages/angular-db/skills/angular-db/SKILL.md": [ - "25b7fee2d442d1c3", - "293aa482f1e106dd", - "400cb43144925775", - "6ba33f3e90701d89", - "b165fab2b59a3492" + "3a0bcdd6c765af48", + "464b244e06f475e1", + "86bdea1b8ff91fb8", + "a19a4ff2326685fb", + "b2cf08104cdeddbc" ], "packages/db/skills/db-core/collection-setup/references/electric-adapter.md": [ - "3fa80e1f966c9bf0", - "8e100aad30cfc960", - "f812b7f924964dab" + "20c0089b1cde36a5", + "97d3555c4042a974", + "ee3fe0e2db71fd53" ], "packages/db/skills/db-core/collection-setup/references/local-adapters.md": [ - "1db77ecbf639e214", - "815689db3c424bed", - "a79babc86533a994", - "e0280f0adaa76fea" + "3f40fac235e48769", + "417103b6203d0548", + "b106b78622c5d827", + "f873986a5c99aa5b" ], "packages/db/skills/db-core/collection-setup/references/powersync-adapter.md": [ - "ad722477b9537355" + "02024a69d58e3f84" ], "packages/db/skills/db-core/collection-setup/references/query-adapter.md": [ - "0b2436b69c112166", - "2dedb5c23e9643f4", - "40888ffca141797f", - "479c6ced9f096fa8", - "50f8e6a2f4715b8c", - "5aeb33df2e687b80", - "5bdb459949daccef", - "8f8649c63feca704", - "b3cb97ac1f7e658a", - "d73c9ed791d09294" + "209b716706b55919", + "2a12cb2c2a7ae211", + "2aa43b643d6e0ae1", + "4e5c42ca12a827b9", + "60d05717c49f7b99", + "775e62be4fcd19c2", + "7f60759fa3b6b7ad", + "852a097e426bcea9", + "9029b244ca7ca414", + "eae8d1649dbc18d5" ], "packages/db/skills/db-core/collection-setup/references/trailbase-adapter.md": [ - "d19456d34f2829fe" + "9f8156f6db6dc3c9" ], "packages/db/skills/db-core/collection-setup/SKILL.md": [ - "27a46b0e250e9b8b", - "4678c7153c57c7ad", - "5c4400cd2b85296c", - "6faada42b5fad1b8", + "09a82388169989af", + "0faec1f503e41efa", + "5422b105923f9a06", + "6ac0adc01e5a5ba2", "73e9be29b2eabd99", - "8e4bb054bababe8a", - "9e3871bbbd486b28", - "a0add18c889fe075", - "b7634e508e808aa1", + "9be49564d49c3537", + "bc9e33c6d8101932", "c2e40e9ea4d731a6", - "cb8024131c6c6499", - "d585a26612f2c861", - "f9ee1b53da4a2522" + "c3982270aa3987bd", + "ca78f76e6a93609e", + "d46dc0fb779f3f2e", + "ea38711d84952007", + "ed585e8184791ac6" ], "packages/db/skills/db-core/custom-adapter/SKILL.md": [ - "3fd318e47cdf7d22", - "54abd1ab294711ab", - "fed30cc8fd62cd2c" + "0482796467157654", + "7007cf2a6462b680", + "af3b7aaf4a476f75" ], "packages/db/skills/db-core/live-queries/references/operators.md": [ "e8a965861e04ca9a" ], "packages/db/skills/db-core/live-queries/SKILL.md": [ - "22ee57e0e1e13df9", + "004641b0cf42bda7", + "1c151b47026bfb63", + "2e839cd44f56421a", "4df1b806a2a71b3a", - "50c570435d86dffc", - "5269bfd07303c2b7", "68539db6b241e22d", - "da372dd6d3097ba5", - "de2fd4bdcdd58cde" + "8250ff79e8c5bce5", + "8d62c98242f89f3a" ], "packages/db/skills/db-core/mutations-optimistic/references/transaction-api.md": [ + "0431b54fc856fd71", "202d1f01879cfdcf", "59e7e51408c2f16d", - "69bbc514541301c6", - "7c427d9e0c514ba7", - "bc0e8d35d9ec9adf", + "bb3d0273cf233b0f", + "e894238baaecc786", "f8d9be1374eedd6e", "ffc88e5fa8edf470" ], "packages/db/skills/db-core/mutations-optimistic/SKILL.md": [ - "0879ec2811ec296b", - "4d0619bec0abf570", - "5fba13ca4647498c", - "b4f9517b78398f1f", - "d06803218b4d2668", - "ed0f4f265b88e861" + "2c5656babca0da98", + "44bdfb42bae08d69", + "51c4242d1a66427e", + "5842b8bdcd0cc5e3", + "995c06ce286a7ce8", + "e3905c73f5d0758d" ], "packages/db/skills/db-core/persistence/SKILL.md": [ "055353a57302f9c0", - "550042d801b88b49", - "906e32cc720a102e", - "92eceb5a26f18594", - "a1f5c7155fd17cc2", - "a45a67fb7467d548" + "0633e54a7e6e829e", + "142207b7c8d3f95e", + "156458f846d8ed40", + "8aa83c82d33133c7", + "f8c3ff40a459e220" ], "packages/db/skills/meta-framework/SKILL.md": [ - "2c23ef1c6d153a6b", - "ac9c6ce8040e97ed", - "b05e12d8d2c0fd9a", - "b7b34c72b79b8bc2" + "4457ee38065812ae", + "52e14f525ffb8f45", + "e08f750ae875cf11", + "eec85afecefbf64e" ], "packages/offline-transactions/skills/offline/SKILL.md": [ - "13adc84e6e5acf58", - "193438a16a3ff165", - "28731f276bad0d7e", - "2e9790f605ddaf82", - "4c0fdefbb072c0a3", - "638e6bd061b3111c", - "67d427db1605b614", - "712fb68b2da868e4", + "05c9ecec5eb6c3e0", + "0edb87a7702e01f3", + "24aea730cbd03c39", + "313ab961f4728419", + "3eba40330af2e29e", + "3f950c8272dbc16e", + "4b2a1e91cf50e338", + "4fcc91057eb5d76d", "777865643b0d79dd", - "969583ae5073e122", - "b67e55956a00d1d4", - "e0669f885466006e", - "e3ccf5757e6c74b8", - "edbb94ac2c0e7585", - "f16e2c64121ac4b2" + "7c4466b67154f21b", + "95f54055d2f8f35c", + "9d7c7778bc5337f4", + "d2dce4009d5fa25e", + "e8cea9396752d82a", + "fe65ab80f7f387c9" ], "packages/react-db/skills/react-db/SKILL.md": [ - "30a5cc1cc6746eab", - "5edaf62175219943", - "91e3b6fe80674fdc", - "9ca257bfb9411532", - "bb6a162d7ecbc818", - "dc77f6fa62cd9451", - "eb2c28e3daffe7db" + "1bcc7b8b735aff4f", + "47c932367431a68f", + "486a2fe62ea43bf6", + "60f0cc2b9131c0ec", + "623a399f123d7ab2", + "96fe86d3776c3201", + "cdb72e95bed7bce8" ], "packages/solid-db/skills/solid-db/SKILL.md": [ - "3409ba5b9fc02338", - "f8202d7a8ac5f9bc" + "10698b13b6021750", + "863b0c944ed3416e" ], "packages/svelte-db/skills/svelte-db/SKILL.md": [ - "284e2907646f52df", - "3fd7db2e8f984e85", - "7b85c967548e02a1", - "fae1b78a475fc6db" + "059b0f080d1d5689", + "853ee9cbae6ed1ed", + "887d8b01bed4ac4e", + "b17807b4cb2ad242" ], "packages/vue-db/skills/vue-db/SKILL.md": [ - "aca20ba5b3b8d041", - "da44abed79157835" + "0f57a4f557133032", + "fe2938818120e68f" ] } From f0504272e83ffa846d740730724fd0024d9cb50b Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 23 Oct 2026 15:53:14 -0600 Subject: [PATCH 24/32] revert(db): keep joins in core with the existing join API Joins are common, and models already know `.join(source, on)` and the `.leftJoin()` family. Making them opt-in saved about 3.6 kB in apps that never join. Grouping, includes, and pooling stay opt-in. - Restore `.join(source, on, type)` and the `leftJoin`, `rightJoin`, `innerJoin`, and `fullJoin` methods. Remove the join operator values, `JoinCondition`, the join runtime on IR join clauses, and `JoinOperatorRequiredError` (245). - Restore the static join compile path, main's optimizer, and main's joins.ts. Public-key reduction stays its own module, so a joined query with a custom key still does not bundle the include materializer. - Tests: a reverse codemod restored main's exact join call at each site. 25 files are identical to main again. - Docs and skills use the existing join API again. The doc example check matches a fence by its backtick count, so a ````ts example that holds a ``` block is read whole. - The TypeScript-modes test covers groupKeys only, and the bundle test treats join modules as core. Typical app: 102,023 to 94,419 B gzip (-7.5%). Co-authored-by: Isaac --- .changeset/opt-in-operators.md | 5 +- docs/contributing/oracle-coverage.md | 20 +- docs/errors.md | 12 +- .../react/reference/functions/useLiveQuery.md | 18 +- .../solid/reference/functions/useLiveQuery.md | 6 +- .../reference/functions/useLiveQuery.md | 6 +- .../vue/reference/functions/useLiveQuery.md | 6 +- docs/guides/live-queries.md | 149 +++--- docs/overview.md | 16 +- .../namespaces/IR/classes/Aggregate.md | 14 +- .../namespaces/IR/classes/CollectionRef.md | 16 +- .../IR/classes/ConditionalSelect.md | 12 +- .../namespaces/IR/classes/DescriptorRef.md | 16 +- .../@tanstack/namespaces/IR/classes/Func.md | 12 +- .../namespaces/IR/classes/IncludesSubquery.md | 26 +- .../namespaces/IR/classes/PropRef.md | 14 +- .../namespaces/IR/classes/QueryRef.md | 14 +- .../namespaces/IR/classes/UnionAll.md | 12 +- .../namespaces/IR/classes/UnionFrom.md | 12 +- .../@tanstack/namespaces/IR/classes/Value.md | 10 +- .../IR/functions/collectCollectionSources.md | 2 +- .../IR/functions/collectPropRefs.md | 2 +- .../IR/functions/collectSourceRefs.md | 2 +- .../IR/functions/createResidualWhere.md | 2 +- .../namespaces/IR/functions/followRef.md | 2 +- .../namespaces/IR/functions/getFromSources.md | 2 +- .../IR/functions/getHavingExpression.md | 2 +- .../IR/functions/getPropRefPropertyPath.md | 2 +- .../IR/functions/getPropRefSourceAlias.md | 2 +- .../IR/functions/getWhereExpression.md | 2 +- .../functions/isBasicOrAggregateExpression.md | 2 +- .../IR/functions/isExpressionLike.md | 2 +- .../IR/functions/isResidualWhere.md | 2 +- .../IR/functions/requireCollectionSource.md | 4 +- .../namespaces/IR/interfaces/JoinClause.md | 209 +------- .../namespaces/IR/interfaces/QueryIR.md | 32 +- .../IR/type-aliases/BasicExpression.md | 2 +- .../IR/type-aliases/CollectionSourceRef.md | 2 +- .../type-aliases/ConditionalSelectBranch.md | 6 +- .../namespaces/IR/type-aliases/From.md | 2 +- .../namespaces/IR/type-aliases/GroupBy.md | 2 +- .../namespaces/IR/type-aliases/Having.md | 2 +- .../type-aliases/IncludesMaterialization.md | 2 +- .../namespaces/IR/type-aliases/Join.md | 2 +- .../namespaces/IR/type-aliases/Limit.md | 2 +- .../namespaces/IR/type-aliases/Offset.md | 2 +- .../namespaces/IR/type-aliases/OrderBy.md | 2 +- .../IR/type-aliases/OrderByClause.md | 6 +- .../IR/type-aliases/OrderByDirection.md | 2 +- .../namespaces/IR/type-aliases/Select.md | 2 +- .../IR/type-aliases/SelectValueExpression.md | 2 +- .../namespaces/IR/type-aliases/Where.md | 2 +- .../IR/variables/INCLUDES_SCALAR_FIELD.md | 2 +- .../AggregateFunctionNotInSelectError.md | 4 +- docs/reference/classes/BaseQueryBuilder.md | 258 ++++++++- .../CannotCombineEmptyExpressionListError.md | 4 +- .../classes/CollectionInputNotFoundError.md | 4 +- .../classes/CollectionPreloadAbortedError.md | 4 +- .../classes/DistinctRequiresSelectError.md | 4 +- .../classes/DuplicateAliasInSubqueryError.md | 4 +- .../classes/EmptyReferencePathError.md | 4 +- .../classes/FnSelectWithGroupByError.md | 4 +- docs/reference/classes/GroupByError.md | 4 +- .../classes/GroupingOperatorRequiredError.md | 4 +- .../classes/HavingRequiresGroupByError.md | 4 +- .../reference/classes/InvalidJoinCondition.md | 4 +- .../InvalidJoinConditionLeftSourceError.md | 4 +- .../InvalidJoinConditionRightSourceError.md | 4 +- .../InvalidJoinConditionSameSourceError.md | 4 +- ...InvalidJoinConditionSourceMismatchError.md | 4 +- .../classes/InvalidSourceTypeError.md | 4 +- .../classes/InvalidStorageDataFormatError.md | 4 +- .../InvalidStorageObjectFormatError.md | 4 +- .../classes/InvalidWhereExpressionError.md | 4 +- .../classes/JoinCollectionNotFoundError.md | 4 +- .../JoinConditionMustBeEqualityError.md | 4 +- docs/reference/classes/JoinError.md | 4 +- .../classes/JoinOperatorRequiredError.md | 218 -------- .../classes/LimitOffsetRequireOrderByError.md | 4 +- .../LoadSubsetOperationAbortedError.md | 4 +- .../classes/LocalStorageCollectionError.md | 4 +- .../classes/MissingAliasInputsError.md | 4 +- ...NonAggregateExpressionNotInGroupByError.md | 4 +- docs/reference/classes/QueryBuilderError.md | 1 - .../classes/QueryCompilationError.md | 4 +- .../classes/QueryMustHaveFromClauseError.md | 4 +- docs/reference/classes/QueryOptimizerError.md | 4 +- docs/reference/classes/SerializationError.md | 4 +- .../classes/SetWindowFindOneLimitError.md | 4 +- .../classes/SetWindowReentrancyError.md | 4 +- .../classes/SetWindowRequiresOrderByError.md | 4 +- docs/reference/classes/StorageError.md | 4 +- .../classes/StorageKeyRequiredError.md | 4 +- docs/reference/classes/SyncCleanupError.md | 4 +- .../classes/SyncTransactionAbortedError.md | 4 +- .../classes/UnhashableQueryIRError.md | 8 +- .../classes/UnknownExpressionTypeError.md | 4 +- .../reference/classes/UnknownFunctionError.md | 4 +- .../UnknownHavingExpressionTypeError.md | 4 +- .../reference/classes/UnsafeAliasPathError.md | 4 +- .../UnsupportedAggregateFunctionError.md | 4 +- .../classes/UnsupportedFnSelectResultError.md | 4 +- .../classes/UnsupportedFromTypeError.md | 4 +- .../classes/UnsupportedJoinSourceTypeError.md | 4 +- .../classes/UnsupportedJoinTypeError.md | 4 +- .../UnsupportedRootScalarSelectError.md | 4 +- docs/reference/functions/compileExpression.md | 4 +- docs/reference/functions/joinOf.md | 47 -- docs/reference/index.md | 9 - docs/reference/interfaces/JoinOperator.md | 254 --------- docs/reference/type-aliases/ExtractContext.md | 2 +- .../type-aliases/InitialQueryBuilder.md | 2 +- docs/reference/type-aliases/JoinCondition.md | 25 - docs/reference/type-aliases/JoinType.md | 10 - .../type-aliases/LiveQueryCollectionUtils.md | 2 +- .../MergeContextForJoinCallback.md | 2 +- docs/reference/type-aliases/QueryBuilder.md | 2 +- docs/reference/type-aliases/QueryResult.md | 2 +- .../type-aliases/SourceClauseContext.md | 2 +- docs/reference/variables/Query.md | 2 +- docs/reference/variables/fullJoin.md | 10 - docs/reference/variables/innerJoin.md | 10 - docs/reference/variables/leftJoin.md | 10 - docs/reference/variables/rightJoin.md | 10 - examples/react/projects/README.md | 28 +- .../src/suites/joins-oracle.suite.ts | 92 +--- .../tests/persisted-readiness-oracle.test.ts | 15 +- packages/db/mangle-cache.json | 3 +- .../db/skills/db-core/live-queries/SKILL.md | 62 +-- packages/db/src/errors.ts | 12 +- packages/db/src/query/builder/index.ts | 169 ++++-- .../db/src/query/builder/join-operators.ts | 88 ---- packages/db/src/query/builder/types.ts | 4 +- packages/db/src/query/compiler/index.ts | 6 +- .../db/src/query/compiler/join-runtime.ts | 18 - packages/db/src/query/compiler/joins.ts | 33 +- packages/db/src/query/compiler/lazy-demand.ts | 32 -- packages/db/src/query/index.ts | 12 - packages/db/src/query/ir.ts | 4 - packages/db/src/query/live/ARCHITECTURE.md | 10 +- .../query/live/collection-config-builder.ts | 56 +- .../db/src/query/live/includes-runtime.ts | 3 +- .../db/src/query/live/public-key-reduction.ts | 2 +- packages/db/src/query/optimizer-joins.ts | 494 ------------------ packages/db/src/query/optimizer.ts | 494 +++++++++++++++++- .../db/tests/collection-auto-index.test.ts | 29 +- .../collection-cleanup-restart-oracle.test.ts | 13 +- ...ction-query-publication-boundaries.test.ts | 4 +- ...ction-subscriber-duplicate-inserts.test.ts | 8 +- packages/db/tests/conformance/suite.ts | 27 +- packages/db/tests/effect.test.ts | 76 +-- packages/db/tests/error-sample-arguments.ts | 1 - packages/db/tests/fixtures/error-codes.json | 1 - .../db/tests/fixtures/error-messages.json | 5 +- .../fixtures/loose-typescript-group-keys.ts | 23 + .../fixtures/loose-typescript-operators.ts | 75 --- .../tests/group-keys-typescript-modes.test.ts | 110 ++++ ...-query-deferred-acquisition-oracle.test.ts | 3 +- packages/db/tests/live-query-options.test.ts | 16 +- .../tests/operator-typescript-modes.test.ts | 125 ----- .../db/tests/opt-in-operator-bundle.test.ts | 31 +- .../query/aggregate-value-contracts.test-d.ts | 8 +- .../tests/query/async-hold-outcomes.test.ts | 18 +- .../db/tests/query/builder/buildQuery.test.ts | 15 +- .../query/builder/callback-types.test-d.ts | 119 ++--- packages/db/tests/query/builder/from.test.ts | 3 +- .../query/builder/functional-variants.test.ts | 33 +- packages/db/tests/query/builder/join.test.ts | 194 +++---- .../tests/query/builder/subqueries.test-d.ts | 25 +- .../db/tests/query/builder/union-all.test.ts | 17 +- packages/db/tests/query/case-when.test-d.ts | 7 +- packages/db/tests/query/case-when.test.ts | 13 +- .../cold-join-reconciliation-oracle.test.ts | 52 +- .../collection-ref-compatibility.test.ts | 2 - .../db/tests/query/compiler/basic.test.ts | 2 - .../tests/query/compiler/lazy-demand.test.ts | 2 - .../tests/query/compiler/subqueries.test.ts | 19 +- .../query/compiler/subquery-caching.test.ts | 4 - packages/db/tests/query/composables.test.ts | 13 +- packages/db/tests/query/distinct.test.ts | 14 +- .../db/tests/query/findone-joins.test-d.ts | 87 ++- .../tests/query/functional-variants.test-d.ts | 25 +- .../tests/query/functional-variants.test.ts | 13 +- .../tests/query/generic-collection.test-d.ts | 12 +- packages/db/tests/query/group-by.test.ts | 7 +- .../identity-output-shape-oracle.test.ts | 27 +- .../includes-alias-shadowing-oracle.test.ts | 301 +++++------ ...ncludes-collection-oracle.property.test.ts | 14 +- .../includes-context-transport-oracle.test.ts | 152 ++---- ...-cross-formulation-oracle.property.test.ts | 7 +- .../tests/query/includes-lazy-loading.test.ts | 7 +- ...ludes-outer-join-route-work-oracle.test.ts | 9 +- .../query/includes-publication-oracle.test.ts | 10 +- .../query/includes-query-shape-oracle.test.ts | 17 +- .../query/includes-scope-identity-oracle.ts | 44 +- .../query/includes-temporal-oracle.test.ts | 5 +- .../includes-work-counter-oracle.test.ts | 18 +- packages/db/tests/query/includes.test.ts | 10 +- packages/db/tests/query/indexes.test.ts | 49 +- .../query/ir-stable-identity-oracle.test.ts | 25 +- .../join-result-key-oracle.property.test.ts | 4 +- .../db/tests/query/join-subquery.test-d.ts | 59 +-- packages/db/tests/query/join-subquery.test.ts | 225 ++++---- packages/db/tests/query/join.test-d.ts | 213 +++----- packages/db/tests/query/join.test.ts | 282 ++++------ .../tests/query/live-query-collection.test.ts | 77 +-- .../query/load-subset-join-dedupe.test.ts | 12 +- ...ad-subset-replay-refinement-oracle.test.ts | 24 +- ...source-readiness-refinement-oracle.test.ts | 8 +- .../tests/query/load-subset-subquery.test.ts | 23 +- .../query/optimizer-semantics-oracle.test.ts | 66 +-- packages/db/tests/query/optimizer.test.ts | 42 -- .../query/optional-fields-negative.test-d.ts | 20 +- .../query/optional-fields-runtime.test.ts | 7 +- packages/db/tests/query/order-by.test.ts | 24 +- .../ordered-lifecycle-oracle.property.test.ts | 24 +- .../ordered-work-oracle.property.test.ts | 117 ++--- .../query/pagination-oracle.property.test.ts | 26 +- .../query/query-api-type-algebra.test-d.ts | 69 +-- .../query/query-api-type-algebra.test.ts | 24 +- packages/db/tests/query/query-once.test.ts | 8 +- .../tests/query/query-while-syncing.test.ts | 38 +- packages/db/tests/query/scheduler.test.ts | 47 +- packages/db/tests/query/select-spread.test.ts | 4 +- .../query/subquery-user-value-oracle.test.ts | 68 +-- .../tests/query/subset-error-matrix.test.ts | 37 +- packages/db/tests/query/union-all.test-d.ts | 46 +- packages/db/tests/query/union-all.test.ts | 71 +-- .../db/tests/query/validate-aliases.test.ts | 56 +- .../query/virtual-row-fields-oracle.test-d.ts | 7 +- .../query/virtual-row-fields-oracle.test.ts | 7 +- .../load-subset-lifecycle-oracle.test.ts | 8 +- .../refetch-ordered-prefix-oracle.test.ts | 7 +- packages/react-db/src/useLiveQuery.ts | 2 +- .../descriptor-query-binding-oracle.test.tsx | 37 +- .../tests/source-id-reuse-oracle.test.tsx | 38 +- packages/react-db/tests/useLiveQuery.test.tsx | 31 +- packages/solid-db/src/useLiveQuery.ts | 2 +- packages/solid-db/tests/useLiveQuery.test.tsx | 25 +- packages/svelte-db/src/useLiveQuery.svelte.ts | 2 +- .../tests/useLiveQuery.svelte.test.ts | 25 +- packages/vue-db/src/useLiveQuery.ts | 2 +- packages/vue-db/tests/useLiveQuery.test.ts | 25 +- scripts/bench/incremental-update.ts | 24 +- scripts/check-doc-snippets.mjs | 10 +- scripts/doc-snippets-baseline.json | 5 +- scripts/size-scenarios/app-join.js | 6 +- scripts/test-minified-db.mjs | 2 +- 248 files changed, 2757 insertions(+), 4607 deletions(-) delete mode 100644 docs/reference/classes/JoinOperatorRequiredError.md delete mode 100644 docs/reference/functions/joinOf.md delete mode 100644 docs/reference/interfaces/JoinOperator.md delete mode 100644 docs/reference/type-aliases/JoinCondition.md delete mode 100644 docs/reference/type-aliases/JoinType.md delete mode 100644 docs/reference/variables/fullJoin.md delete mode 100644 docs/reference/variables/innerJoin.md delete mode 100644 docs/reference/variables/leftJoin.md delete mode 100644 docs/reference/variables/rightJoin.md delete mode 100644 packages/db/src/query/builder/join-operators.ts delete mode 100644 packages/db/src/query/compiler/join-runtime.ts delete mode 100644 packages/db/src/query/compiler/lazy-demand.ts delete mode 100644 packages/db/src/query/optimizer-joins.ts create mode 100644 packages/db/tests/fixtures/loose-typescript-group-keys.ts delete mode 100644 packages/db/tests/fixtures/loose-typescript-operators.ts create mode 100644 packages/db/tests/group-keys-typescript-modes.test.ts delete mode 100644 packages/db/tests/operator-typescript-modes.test.ts diff --git a/.changeset/opt-in-operators.md b/.changeset/opt-in-operators.md index c6eecf8085..1333913335 100644 --- a/.changeset/opt-in-operators.md +++ b/.changeset/opt-in-operators.md @@ -7,10 +7,9 @@ '@tanstack/angular-db': patch --- -Breaking: joins, grouping, includes, and pooling are now opt-in. An app bundles an operator's code only when it imports the value for that operator. A typical app is about 10 kB smaller after gzip. This app has a query collection, a filtered live query, and React `useLiveQuery`. +Breaking: grouping, includes, and pooling are now opt-in. An app bundles an operator's code only when it imports the value for that operator. Joins do not change. A typical app is about 7.5 kB smaller after gzip. This app has a query collection, a filtered live query, and React `useLiveQuery`. -- Joins take a join operator. Change `.join(source, on)` to `.join(leftJoin(source, on))`, and `.innerJoin(source, on)` to `.join(innerJoin(source, on))`. `rightJoin`, `fullJoin`, and `joinOf(type, source, on)` work the same way. The `.join(source, on)` call shape throws `JoinOperatorRequiredError`. This release removes the `.leftJoin()`, `.innerJoin()`, `.rightJoin()`, and `.fullJoin()` methods. - An include needs `include()`, `toArray()`, or `materialize()`. Change `issues: q.from(...)` in `select()` to `issues: include(q.from(...))`. A subquery in `select()` without one of these throws `IncludeOperatorRequiredError`. - Aggregates such as `count()` need no change. A `groupBy` with no aggregate takes `groupKeys()`: `.groupBy(groupKeys(({ todo }) => todo.status))`. Without it, the query throws `GroupingOperatorRequiredError`. - Hooks do not pool live queries by default. To share one partition among many equality-filtered queries, pass `pool: pooledLiveQueries` in the hook's query config: `useLiveQuery({ query, pool: pooledLiveQueries })`. Results do not change. Angular's `{ params, query }` form takes `pool` the same way. For adapter authors: the `pool` option of `resolveLiveQueryValue` is now `pooledLiveQueries` or `false`, not a boolean. -- Code that builds query IR by hand must supply the operator code. `IR.Aggregate` takes the grouping compiler as its third argument. `IR.IncludesSubquery` takes the include runtime as its fifth argument. `IR.JoinClause` has a required `runtime` field. Queries that the query builder builds need no change. +- Code that builds query IR by hand must supply the operator code. `IR.Aggregate` takes the grouping compiler as its third argument. `IR.IncludesSubquery` takes the include runtime as its fifth argument. Queries that the query builder builds need no change. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 1861eadbd7..19008c9489 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -3769,16 +3769,16 @@ review record is `docs/contributing/oracle-reviews/issue-2058-paced-serializatio ## Opt-in operator bundles -Joins, grouping, includes, and pooling ship only with the value that names -them: a join operator, an aggregate or `groupKeys`, an include function, or -`pooledLiveQueries`. `packages/db/tests/opt-in-operator-bundle.test.ts` owns -that law. It bundles small apps from source with esbuild and reads which -modules reach the output. Each of the 15 operator values runs alone, and every -one of the 16 combinations of operator families runs with rotating values. An -operator's modules must be present exactly when the app imports one of its -values, and public-key reduction exactly when it imports a join or an include. -The single-value runs are positive controls, so a renamed or moved module -cannot pass the absence checks. A mutant that statically imports +Grouping, includes, and pooling ship only with the value that names them: an +aggregate or `groupKeys`, an include function, or `pooledLiveQueries`. Joins +are core. `packages/db/tests/opt-in-operator-bundle.test.ts` owns that law. It +bundles small apps from source with esbuild and reads which modules reach the +output. Each of the 10 operator values runs alone, and every one of the 8 +combinations of operator families runs with rotating values. An operator's +modules must be present exactly when the app imports one of its values. The +join compiler and public-key reduction must be present in every app. The +single-value runs are positive controls, so a renamed or moved module cannot +pass the absence checks. A mutant that statically imports `BucketFacadeAdapter` into the live-query builder, and one that uses a `group-by.ts` helper from the live-query Collection builder, fail it. diff --git a/docs/errors.md b/docs/errors.md index 150b17db77..ef250915d5 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -490,7 +490,7 @@ Invalid source for from clause: Expected an object with a single key-value pair ``` ```text -Invalid source for join clause: Expected an object with a single key-value pair like { alias: collection }. For example: .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Got: number +Invalid source for join clause: Expected an object with a single key-value pair like { alias: collection }. For example: .join({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id)). Got: number ``` ```text @@ -990,16 +990,6 @@ Development builds show the full message. For example: setWindow() cannot change the limit of a findOne() query. ``` - - -## Error 245: `JoinOperatorRequiredError` - -Development builds show the full message. For example: - -```text -.join() takes a join operator, such as .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Import leftJoin, innerJoin, rightJoin, fullJoin, or joinOf from @tanstack/db. Got: object -``` - ## Error 246: `GroupingOperatorRequiredError` diff --git a/docs/framework/react/reference/functions/useLiveQuery.md b/docs/framework/react/reference/functions/useLiveQuery.md index f4558cc4cf..ccd24250e7 100644 --- a/docs/framework/react/reference/functions/useLiveQuery.md +++ b/docs/framework/react/reference/functions/useLiveQuery.md @@ -163,7 +163,7 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -354,7 +354,7 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -548,7 +548,7 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -747,7 +747,7 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -957,7 +957,7 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -1284,7 +1284,7 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -1469,7 +1469,7 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -1658,7 +1658,7 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -1990,7 +1990,7 @@ const { data, isEnabled } = useLiveQuery({ const { data } = useLiveQuery({ query: (q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ diff --git a/docs/framework/solid/reference/functions/useLiveQuery.md b/docs/framework/solid/reference/functions/useLiveQuery.md index 39421244bc..7df0110ac5 100644 --- a/docs/framework/solid/reference/functions/useLiveQuery.md +++ b/docs/framework/solid/reference/functions/useLiveQuery.md @@ -54,7 +54,7 @@ const todosQuery = useLiveQuery( // Join pattern const personIssues = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -154,7 +154,7 @@ const todosQuery = useLiveQuery( // Join pattern const personIssues = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -401,7 +401,7 @@ const todosQuery = useLiveQuery( // Join pattern const personIssues = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ diff --git a/docs/framework/svelte/reference/functions/useLiveQuery.md b/docs/framework/svelte/reference/functions/useLiveQuery.md index f35f499b1f..36ecc63699 100644 --- a/docs/framework/svelte/reference/functions/useLiveQuery.md +++ b/docs/framework/svelte/reference/functions/useLiveQuery.md @@ -98,7 +98,7 @@ const { data, isLoading, isError } = $derived(query) // Join pattern const issuesQuery = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -224,7 +224,7 @@ const { data, isLoading, isError } = $derived(query) // Join pattern const issuesQuery = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -506,7 +506,7 @@ const { data, isLoading, isError } = $derived(query) // Join pattern const issuesQuery = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ diff --git a/docs/framework/vue/reference/functions/useLiveQuery.md b/docs/framework/vue/reference/functions/useLiveQuery.md index 21aea1fe6e..ec49913343 100644 --- a/docs/framework/vue/reference/functions/useLiveQuery.md +++ b/docs/framework/vue/reference/functions/useLiveQuery.md @@ -64,7 +64,7 @@ const { data, state } = useLiveQuery( // Join pattern const { data } = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -150,7 +150,7 @@ const { data, state } = useLiveQuery( // Join pattern const { data } = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ @@ -383,7 +383,7 @@ const { data, state } = useLiveQuery( // Join pattern const { data } = useLiveQuery((q) => q.from({ issues: issueCollection }) - .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + .join({ persons: personCollection }, ({ issues, persons }) => eq(issues.userId, persons.id) )) .select(({ issues, persons }) => ({ diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index dd8fc21f70..544181644a 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -892,7 +892,7 @@ For a complete list of available functions, see the [Expression Functions Refere ## Joins -Use `join` with a join operator to combine data from multiple collections. Import `leftJoin`, `innerJoin`, `rightJoin`, or `fullJoin` from `@tanstack/db` and pass it to `join`. Join conditions are equalities, optionally combined with `and()`. The operator brings the join code with it, so an app that never joins does not bundle it. +Use `join` to combine data from multiple collections. Joins default to `left` join type and support equality conditions combined with `and()`. Joins in TanStack DB are a way to combine data from multiple collections, and are conceptually very similar to SQL joins. When two collections are joined, the result is a new collection that contains the combined data as single rows. The new collection is a live query collection, and will automatically update when the underlying data changes. @@ -905,27 +905,18 @@ The result type of a join will take into account the join type, with the optiona ### Method Signature -```ts -join(operator: JoinOperator): Query - -leftJoin( +```ts nocheck +join( { [alias]: Collection | Query }, condition: (row: TRow) => Expression, // `eq(...)` or `and(eq(...), ...)` -): JoinOperator // also innerJoin, rightJoin, fullJoin - -joinOf( - joinType: 'left' | 'right' | 'inner' | 'full', - { [alias]: Collection | Query }, - condition: (row: TRow) => Expression, -): JoinOperator + joinType?: 'left' | 'right' | 'inner' | 'full' +): Query ``` **Parameters:** - `aliases` - An object where keys are alias names and values are collections or subqueries to join - `condition` - A callback function that receives the combined row object and returns an equality or a conjunction of equalities -- `joinType` - For `joinOf`, a join type chosen at run time - -Pass the operator inline, as in `.join(leftJoin(...))`, so its condition callback is typed from the query. +- `joinType` - Optional join type: `'left'` (default), `'right'`, `'inner'`, or `'full'` ### Basic Joins @@ -935,9 +926,9 @@ Join users with their posts: const userPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => + .join({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId) - )) + ) ) /* @@ -962,9 +953,9 @@ Use `and()` when rows must match on more than one field: ```ts const inventory = createLiveQueryCollection((q) => q.from({ product: productsCollection }) - .join(innerJoin({ stock: inventoryCollection }, ({ product, stock }) => + .innerJoin({ stock: inventoryCollection }, ({ product, stock }) => and(eq(product.sku, stock.sku), eq(product.region, stock.region)) - )) + ) ) ``` @@ -992,18 +983,21 @@ no keyed demand when lazy loading applies. ### Join Types -Choose the join type with the operator you import: +Specify the join type as the third parameter: ```ts const activeUserPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(innerJoin({ post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId))) + .join( + { post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId), + 'inner', // `inner`, `left`, `right` or `full` + ) ) ``` -Each join type has its own operator: +Or using the aliases `leftJoin`, `rightJoin`, `innerJoin` and `fullJoin` methods: ### Left Join ```ts @@ -1011,8 +1005,10 @@ Each join type has its own operator: const allUsers = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(leftJoin({ post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId))) + .leftJoin( + { post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId), + ) ) /* @@ -1031,8 +1027,10 @@ Result type: const allPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(rightJoin({ post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId))) + .rightJoin( + { post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId), + ) ) /* @@ -1051,8 +1049,10 @@ Result type: const activeUserPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(innerJoin({ post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId))) + .innerJoin( + { post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId), + ) ) /* @@ -1071,8 +1071,10 @@ Result type: const allUsersAndPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(fullJoin({ post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId))) + .fullJoin( + { post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId), + ) ) /* @@ -1092,12 +1094,12 @@ Chain multiple joins in a single query: const userPostComments = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => + .join({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId) - )) - .join(leftJoin({ comment: commentsCollection }, ({ post, comment }) => + ) + .join({ comment: commentsCollection }, ({ post, comment }) => eq(post.id, comment.postId) - )) + ) .select(({ user, post, comment }) => ({ userName: user.name, postTitle: post.title, @@ -1106,30 +1108,6 @@ const userPostComments = createLiveQueryCollection((q) => ) ``` -### Reusable Join Operators - -A join operator is a value, so you can build one once and use it in many queries. Inside `.join()`, the operator gets the query's sources from the query. An operator built outside `.join()` does not see the query, so give it the query's context explicitly. Otherwise its condition is untyped, and a wrong field name compiles without an error: - -```ts -import { createLiveQueryCollection, eq, leftJoin } from '@tanstack/db' -import type { ContextFromSource } from '@tanstack/db' - -// The sources that queries using this operator start from. -type UserQuery = ContextFromSource<{ user: typeof usersCollection }> - -export const withPosts = leftJoin<{ post: typeof postsCollection }, UserQuery>( - { post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId), -) - -const usersWithPosts = createLiveQueryCollection((q) => - q - .from({ user: usersCollection }) - .join(withPosts) - .select(({ user, post }) => ({ name: user.name, title: post.title })), -) -``` - ## Subqueries Subqueries allow you to use the result of one query as input to another, they are embedded within the query itself and are compile to a single query pipeline. They are very similar to SQL subqueries that are executed as part of a single operation. @@ -1153,9 +1131,9 @@ const activeUserPosts = createCollection(liveQueryCollectionOptions({ // Use the subquery in the main query return q .from({ activeUser: activeUsers }) - .join(leftJoin({ post: postsCollection }, ({ activeUser, post }) => + .join({ post: postsCollection }, ({ activeUser, post }) => eq(activeUser.id, post.userId) - )) + ) } })) ``` @@ -1177,9 +1155,9 @@ const userRecentPosts = createCollection(liveQueryCollectionOptions({ // Use the subquery in the main query return q .from({ user: usersCollection }) - .join(leftJoin({ recentPost: recentPosts }, ({ user, recentPost }) => + .join({ recentPost: recentPosts }, ({ user, recentPost }) => eq(user.id, recentPost.userId) - )) + ) } })) ``` @@ -1199,12 +1177,12 @@ const complexQuery = createCollection(liveQueryCollectionOptions({ // Use the same subquery multiple times return q .from({ activeUser: activeUsers }) - .join(leftJoin({ post: postsCollection }, ({ activeUser, post }) => + .join({ post: postsCollection }, ({ activeUser, post }) => eq(activeUser.id, post.userId) - )) - .join(leftJoin({ comment: commentsCollection }, ({ activeUser, comment }) => + ) + .join({ comment: commentsCollection }, ({ activeUser, comment }) => eq(activeUser.id, comment.userId) - )) + ) } })) ``` @@ -1216,7 +1194,7 @@ In this example, the `activeUsers` subquery is used twice but executed only once Build complex queries with multiple levels of nesting: ```ts -import { count, createCollection, eq, leftJoin, liveQueryCollectionOptions } from '@tanstack/db' +import { count, createCollection, eq, liveQueryCollectionOptions } from '@tanstack/db' const topUsers = createCollection(liveQueryCollectionOptions({ query: (q) => { @@ -1232,9 +1210,9 @@ const topUsers = createCollection(liveQueryCollectionOptions({ // Build the user stats subquery const userStats = q .from({ user: usersCollection }) - .join(leftJoin({ postCount: postCounts }, ({ user, postCount }) => + .join({ postCount: postCounts }, ({ user, postCount }) => eq(user.id, postCount.userId) - )) + ) .select(({ user, postCount }) => ({ id: user.id, name: user.name, @@ -1530,7 +1508,7 @@ const departments = createLiveQueryCollection((q) => ) ``` -Like a [reusable join operator](#reusable-join-operators), `groupKeys()` built outside `.groupBy()` needs the query's context as a type argument. Without it, the callback is untyped: +`groupKeys()` built outside `.groupBy()` needs the query's context as a type argument. Without it, the callback is untyped: ```ts import { groupKeys } from '@tanstack/db' @@ -2140,9 +2118,9 @@ const activeUsers = createLiveQueryCollection((q) => const activeUserPosts = createLiveQueryCollection((q) => q .from({ user: activeUsers }) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => + .join({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId) - )) + ) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, @@ -2155,7 +2133,7 @@ const activeUserPosts = createLiveQueryCollection((q) => You can use the `Query` class to create reusable query definitions. This is useful for building complex queries where you want to reuse the same query builder instance multiple times throughout your application. ```ts -import { createLiveQueryCollection, eq, leftJoin, Query } from '@tanstack/db' +import { Query, createLiveQueryCollection, eq } from '@tanstack/db' // Create a reusable query builder const userQuery = new Query() @@ -2174,9 +2152,9 @@ const activeUsers = createLiveQueryCollection({ const userPosts = createLiveQueryCollection((q) => q .from({ activeUser: userQuery }) - .join(leftJoin({ post: postsCollection }, ({ activeUser, post }) => + .join({ post: postsCollection }, ({ activeUser, post }) => eq(activeUser.id, post.userId) - )) + ) ) ``` @@ -2273,9 +2251,9 @@ const highValueCustomers = createCollection(liveQueryCollectionOptions({ query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ order: ordersCollection }, ({ user, order }) => + .join({ order: ordersCollection }, ({ user, order }) => eq(user.id, order.userId) - )) + ) .where(isHighValueCustomer) .select(({ user, order }) => ({ userName: user.name, @@ -2554,9 +2532,9 @@ createEffect({ query: (q) => q .from({ user: usersCollection }) - .join(leftJoin({ post: postsCollection }, ({ user, post }) => + .join({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId) - )) + ) .select(({ user, post }) => ({ userName: user.name, postTitle: post.title, @@ -2649,16 +2627,16 @@ or hot-path queries. Here's a more complete example showing an effect that monitors order status changes and sends notifications: ```ts -import { createEffect, eq, leftJoin } from '@tanstack/db' +import { createEffect, eq } from '@tanstack/db' const orderEffect = createEffect({ id: 'order-status-monitor', query: (q) => q .from({ order: ordersCollection }) - .join(leftJoin({ customer: customersCollection }, ({ order, customer }) => + .join({ customer: customersCollection }, ({ order, customer }) => eq(order.customerId, customer.id) - )) + ) .where(({ order }) => eq(order.status, 'shipped')) .select(({ order, customer }) => ({ orderId: order.id, @@ -2745,9 +2723,10 @@ These functions are particularly important when working with joins and optional const usersWithoutProfiles = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join(leftJoin({ profile: profilesCollection }, + .leftJoin( + { profile: profilesCollection }, ({ user, profile }) => eq(user.id, profile.userId) - )) + ) .where(({ profile }) => isUndefined(profile)) ) diff --git a/docs/overview.md b/docs/overview.md index fb3e727997..454afb1877 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -323,15 +323,18 @@ You can also query across collections with joins: ```ts import { useLiveQuery } from '@tanstack/react-db' -import { eq, innerJoin } from '@tanstack/db' +import { eq } from '@tanstack/db' const Todos = () => { const { data: todos } = useLiveQuery({ query: (q) => q .from({ todos: todoCollection }) - .join(innerJoin({ lists: listCollection }, - ({ todos, lists }) => eq(lists.id, todos.listId))) + .join( + { lists: listCollection }, + ({ todos, lists }) => eq(lists.id, todos.listId), + 'inner' + ) .where(({ lists }) => eq(lists.active, true)) .select(({ todos, lists }) => ({ id: todos.id, @@ -494,8 +497,11 @@ const Todos = () => { query: (q) => q .from({ todo: todoCollection }) - .join(innerJoin({ list: listCollection }, - ({ todo, list }) => eq(list.id, todo.list_id))) + .join( + { list: listCollection }, + ({ todo, list }) => eq(list.id, todo.list_id), + "inner" + ) .where(({ list }) => eq(list.active, true)) .select(({ todo, list }) => ({ id: todo.id, diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md b/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md index 7583c23076..80868212b3 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md @@ -3,7 +3,7 @@ id: Aggregate title: Aggregate --- -Defined in: [packages/db/src/query/ir.ts:269](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L269) +Defined in: [packages/db/src/query/ir.ts:265](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L265) ## Extends @@ -26,7 +26,7 @@ new Aggregate( grouping): Aggregate; ``` -Defined in: [packages/db/src/query/ir.ts:271](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L271) +Defined in: [packages/db/src/query/ir.ts:267](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L267) #### Parameters @@ -66,7 +66,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) **`Internal`** @@ -86,7 +86,7 @@ BaseExpression.__returnType args: BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:273](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L273) +Defined in: [packages/db/src/query/ir.ts:269](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L269) *** @@ -96,7 +96,7 @@ Defined in: [packages/db/src/query/ir.ts:273](https://github.com/TanStack/db/blo grouping: object; ``` -Defined in: [packages/db/src/query/ir.ts:275](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L275) +Defined in: [packages/db/src/query/ir.ts:271](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L271) #### processGroupBy() @@ -203,7 +203,7 @@ A transformed BasicExpression that references computed values instead of raw exp name: string; ``` -Defined in: [packages/db/src/query/ir.ts:272](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L272) +Defined in: [packages/db/src/query/ir.ts:268](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L268) *** @@ -213,7 +213,7 @@ Defined in: [packages/db/src/query/ir.ts:272](https://github.com/TanStack/db/blo type: "agg"; ``` -Defined in: [packages/db/src/query/ir.ts:270](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L270) +Defined in: [packages/db/src/query/ir.ts:266](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L266) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md b/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md index b6beb43436..e43078d92a 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md @@ -3,7 +3,7 @@ id: CollectionRef title: CollectionRef --- -Defined in: [packages/db/src/query/ir.ts:95](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L95) +Defined in: [packages/db/src/query/ir.ts:91](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L91) ## Extends @@ -20,7 +20,7 @@ new CollectionRef( bindingId?): CollectionRef; ``` -Defined in: [packages/db/src/query/ir.ts:102](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L102) +Defined in: [packages/db/src/query/ir.ts:98](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L98) #### Parameters @@ -54,7 +54,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) **`Internal`** @@ -74,7 +74,7 @@ BaseExpression.__returnType alias: string; ``` -Defined in: [packages/db/src/query/ir.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L100) +Defined in: [packages/db/src/query/ir.ts:96](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L96) *** @@ -84,7 +84,7 @@ Defined in: [packages/db/src/query/ir.ts:100](https://github.com/TanStack/db/blo collection: CollectionImpl; ``` -Defined in: [packages/db/src/query/ir.ts:99](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L99) +Defined in: [packages/db/src/query/ir.ts:95](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L95) *** @@ -94,7 +94,7 @@ Defined in: [packages/db/src/query/ir.ts:99](https://github.com/TanStack/db/blob type: "collectionRef"; ``` -Defined in: [packages/db/src/query/ir.ts:96](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L96) +Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) #### Overrides @@ -112,7 +112,7 @@ BaseExpression.type get bindingId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:118](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L118) +Defined in: [packages/db/src/query/ir.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L114) ##### Returns @@ -128,7 +128,7 @@ Defined in: [packages/db/src/query/ir.ts:118](https://github.com/TanStack/db/blo get sourceId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L114) +Defined in: [packages/db/src/query/ir.ts:110](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L110) Opaque runtime identity; aliases are lexical names only. diff --git a/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md b/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md index 48d6ab444b..7563d193e6 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md @@ -3,7 +3,7 @@ id: ConditionalSelect title: ConditionalSelect --- -Defined in: [packages/db/src/query/ir.ts:317](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L317) +Defined in: [packages/db/src/query/ir.ts:313](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L313) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:317](https://github.com/TanStack/db/blo new ConditionalSelect(branches, defaultValue?): ConditionalSelect; ``` -Defined in: [packages/db/src/query/ir.ts:319](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L319) +Defined in: [packages/db/src/query/ir.ts:315](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L315) #### Parameters @@ -47,7 +47,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) **`Internal`** @@ -67,7 +67,7 @@ BaseExpression.__returnType branches: ConditionalSelectBranch[]; ``` -Defined in: [packages/db/src/query/ir.ts:320](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L320) +Defined in: [packages/db/src/query/ir.ts:316](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L316) *** @@ -77,7 +77,7 @@ Defined in: [packages/db/src/query/ir.ts:320](https://github.com/TanStack/db/blo optional defaultValue: SelectValueExpression; ``` -Defined in: [packages/db/src/query/ir.ts:321](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L321) +Defined in: [packages/db/src/query/ir.ts:317](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L317) *** @@ -87,7 +87,7 @@ Defined in: [packages/db/src/query/ir.ts:321](https://github.com/TanStack/db/blo type: "conditionalSelect"; ``` -Defined in: [packages/db/src/query/ir.ts:318](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L318) +Defined in: [packages/db/src/query/ir.ts:314](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L314) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md b/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md index 02d169b3ac..8ea97d4fbc 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/DescriptorRef.md @@ -3,7 +3,7 @@ id: DescriptorRef title: DescriptorRef --- -Defined in: [packages/db/src/query/ir.ts:123](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L123) +Defined in: [packages/db/src/query/ir.ts:119](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L119) ## Extends @@ -20,7 +20,7 @@ new DescriptorRef( bindingId?): DescriptorRef; ``` -Defined in: [packages/db/src/query/ir.ts:127](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L127) +Defined in: [packages/db/src/query/ir.ts:123](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L123) #### Parameters @@ -54,7 +54,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) **`Internal`** @@ -74,7 +74,7 @@ BaseExpression.__returnType alias: string; ``` -Defined in: [packages/db/src/query/ir.ts:129](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L129) +Defined in: [packages/db/src/query/ir.ts:125](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L125) *** @@ -84,7 +84,7 @@ Defined in: [packages/db/src/query/ir.ts:129](https://github.com/TanStack/db/blo descriptor: CollectionOptionsIdentity; ``` -Defined in: [packages/db/src/query/ir.ts:128](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L128) +Defined in: [packages/db/src/query/ir.ts:124](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L124) *** @@ -94,7 +94,7 @@ Defined in: [packages/db/src/query/ir.ts:128](https://github.com/TanStack/db/blo type: "descriptorRef"; ``` -Defined in: [packages/db/src/query/ir.ts:124](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L124) +Defined in: [packages/db/src/query/ir.ts:120](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L120) #### Overrides @@ -112,7 +112,7 @@ BaseExpression.type get bindingId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:141](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L141) +Defined in: [packages/db/src/query/ir.ts:137](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L137) ##### Returns @@ -128,7 +128,7 @@ Defined in: [packages/db/src/query/ir.ts:141](https://github.com/TanStack/db/blo get sourceId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:137](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L137) +Defined in: [packages/db/src/query/ir.ts:133](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L133) Opaque runtime identity for this unbound source placement. diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Func.md b/docs/reference/@tanstack/namespaces/IR/classes/Func.md index 485e4bc2f4..c906b727ba 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Func.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Func.md @@ -3,7 +3,7 @@ id: Func title: Func --- -Defined in: [packages/db/src/query/ir.ts:254](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L254) +Defined in: [packages/db/src/query/ir.ts:250](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L250) ## Extends @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/ir.ts:254](https://github.com/TanStack/db/blo new Func(name, args): Func; ``` -Defined in: [packages/db/src/query/ir.ts:256](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L256) +Defined in: [packages/db/src/query/ir.ts:252](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L252) #### Parameters @@ -53,7 +53,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) **`Internal`** @@ -73,7 +73,7 @@ BaseExpression.__returnType args: BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:258](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L258) +Defined in: [packages/db/src/query/ir.ts:254](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L254) *** @@ -83,7 +83,7 @@ Defined in: [packages/db/src/query/ir.ts:258](https://github.com/TanStack/db/blo name: string; ``` -Defined in: [packages/db/src/query/ir.ts:257](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L257) +Defined in: [packages/db/src/query/ir.ts:253](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L253) *** @@ -93,7 +93,7 @@ Defined in: [packages/db/src/query/ir.ts:257](https://github.com/TanStack/db/blo type: "func"; ``` -Defined in: [packages/db/src/query/ir.ts:255](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L255) +Defined in: [packages/db/src/query/ir.ts:251](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L251) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md b/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md index 9604322fd8..12e597d9f3 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md @@ -3,7 +3,7 @@ id: IncludesSubquery title: IncludesSubquery --- -Defined in: [packages/db/src/query/ir.ts:291](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L291) +Defined in: [packages/db/src/query/ir.ts:287](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L287) ## Extends @@ -26,7 +26,7 @@ new IncludesSubquery( scalarField?): IncludesSubquery; ``` -Defined in: [packages/db/src/query/ir.ts:293](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L293) +Defined in: [packages/db/src/query/ir.ts:289](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L289) #### Parameters @@ -90,7 +90,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) **`Internal`** @@ -110,7 +110,7 @@ BaseExpression.__returnType childCorrelationField: PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:296](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L296) +Defined in: [packages/db/src/query/ir.ts:292](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L292) *** @@ -120,7 +120,7 @@ Defined in: [packages/db/src/query/ir.ts:296](https://github.com/TanStack/db/blo correlationField: PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:295](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L295) +Defined in: [packages/db/src/query/ir.ts:291](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L291) *** @@ -130,7 +130,7 @@ Defined in: [packages/db/src/query/ir.ts:295](https://github.com/TanStack/db/blo fieldName: string; ``` -Defined in: [packages/db/src/query/ir.ts:297](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L297) +Defined in: [packages/db/src/query/ir.ts:293](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L293) *** @@ -140,7 +140,7 @@ Defined in: [packages/db/src/query/ir.ts:297](https://github.com/TanStack/db/blo materialization: IncludesMaterialization; ``` -Defined in: [packages/db/src/query/ir.ts:302](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L302) +Defined in: [packages/db/src/query/ir.ts:298](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L298) *** @@ -150,7 +150,7 @@ Defined in: [packages/db/src/query/ir.ts:302](https://github.com/TanStack/db/blo optional parentFilters: Where[]; ``` -Defined in: [packages/db/src/query/ir.ts:300](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L300) +Defined in: [packages/db/src/query/ir.ts:296](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L296) *** @@ -160,7 +160,7 @@ Defined in: [packages/db/src/query/ir.ts:300](https://github.com/TanStack/db/blo optional parentProjection: PropRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:301](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L301) +Defined in: [packages/db/src/query/ir.ts:297](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L297) *** @@ -170,7 +170,7 @@ Defined in: [packages/db/src/query/ir.ts:301](https://github.com/TanStack/db/blo query: QueryIR; ``` -Defined in: [packages/db/src/query/ir.ts:294](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L294) +Defined in: [packages/db/src/query/ir.ts:290](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L290) *** @@ -180,7 +180,7 @@ Defined in: [packages/db/src/query/ir.ts:294](https://github.com/TanStack/db/blo runtime: object; ``` -Defined in: [packages/db/src/query/ir.ts:299](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L299) +Defined in: [packages/db/src/query/ir.ts:295](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L295) #### createBucketFacades() @@ -230,7 +230,7 @@ adapter resolves those references after the graph reaches quiescence. optional scalarField: string; ``` -Defined in: [packages/db/src/query/ir.ts:303](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L303) +Defined in: [packages/db/src/query/ir.ts:299](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L299) *** @@ -240,7 +240,7 @@ Defined in: [packages/db/src/query/ir.ts:303](https://github.com/TanStack/db/blo type: "includesSubquery"; ``` -Defined in: [packages/db/src/query/ir.ts:292](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L292) +Defined in: [packages/db/src/query/ir.ts:288](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L288) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md b/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md index d5716c4865..9822bcd13e 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md @@ -3,7 +3,7 @@ id: PropRef title: PropRef --- -Defined in: [packages/db/src/query/ir.ts:209](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L209) +Defined in: [packages/db/src/query/ir.ts:205](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L205) ## Extends @@ -26,7 +26,7 @@ new PropRef( bindingId?): PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:213](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L213) +Defined in: [packages/db/src/query/ir.ts:209](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L209) #### Parameters @@ -60,7 +60,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) **`Internal`** @@ -80,7 +80,7 @@ BaseExpression.__returnType readonly optional bindingId: string; ``` -Defined in: [packages/db/src/query/ir.ts:212](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L212) +Defined in: [packages/db/src/query/ir.ts:208](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L208) *** @@ -90,7 +90,7 @@ Defined in: [packages/db/src/query/ir.ts:212](https://github.com/TanStack/db/blo path: string[]; ``` -Defined in: [packages/db/src/query/ir.ts:214](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L214) +Defined in: [packages/db/src/query/ir.ts:210](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L210) *** @@ -100,7 +100,7 @@ Defined in: [packages/db/src/query/ir.ts:214](https://github.com/TanStack/db/blo readonly optional sourceAlias: string; ``` -Defined in: [packages/db/src/query/ir.ts:211](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L211) +Defined in: [packages/db/src/query/ir.ts:207](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L207) *** @@ -110,7 +110,7 @@ Defined in: [packages/db/src/query/ir.ts:211](https://github.com/TanStack/db/blo type: "ref"; ``` -Defined in: [packages/db/src/query/ir.ts:210](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L210) +Defined in: [packages/db/src/query/ir.ts:206](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L206) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md b/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md index 1e8d2f148c..f057878bbc 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md @@ -3,7 +3,7 @@ id: QueryRef title: QueryRef --- -Defined in: [packages/db/src/query/ir.ts:164](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L164) +Defined in: [packages/db/src/query/ir.ts:160](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L160) ## Extends @@ -20,7 +20,7 @@ new QueryRef( bindingId?): QueryRef; ``` -Defined in: [packages/db/src/query/ir.ts:167](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L167) +Defined in: [packages/db/src/query/ir.ts:163](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L163) #### Parameters @@ -54,7 +54,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) **`Internal`** @@ -74,7 +74,7 @@ BaseExpression.__returnType alias: string; ``` -Defined in: [packages/db/src/query/ir.ts:169](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L169) +Defined in: [packages/db/src/query/ir.ts:165](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L165) *** @@ -84,7 +84,7 @@ Defined in: [packages/db/src/query/ir.ts:169](https://github.com/TanStack/db/blo query: QueryIR; ``` -Defined in: [packages/db/src/query/ir.ts:168](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L168) +Defined in: [packages/db/src/query/ir.ts:164](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L164) *** @@ -94,7 +94,7 @@ Defined in: [packages/db/src/query/ir.ts:168](https://github.com/TanStack/db/blo type: "queryRef"; ``` -Defined in: [packages/db/src/query/ir.ts:165](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L165) +Defined in: [packages/db/src/query/ir.ts:161](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L161) #### Overrides @@ -112,7 +112,7 @@ BaseExpression.type get bindingId(): string; ``` -Defined in: [packages/db/src/query/ir.ts:176](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L176) +Defined in: [packages/db/src/query/ir.ts:172](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L172) ##### Returns diff --git a/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md b/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md index 36f077a7aa..ad1fcecb7e 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md @@ -3,7 +3,7 @@ id: UnionAll title: UnionAll --- -Defined in: [packages/db/src/query/ir.ts:192](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L192) +Defined in: [packages/db/src/query/ir.ts:188](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L188) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:192](https://github.com/TanStack/db/blo new UnionAll(queries): UnionAll; ``` -Defined in: [packages/db/src/query/ir.ts:200](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L200) +Defined in: [packages/db/src/query/ir.ts:196](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L196) Result-level UNION ALL. Downstream query clauses see the union result row shape, not the branch source aliases. Optimizers may push safe operations @@ -48,7 +48,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) **`Internal`** @@ -68,7 +68,7 @@ BaseExpression.__returnType queries: QueryIR[]; ``` -Defined in: [packages/db/src/query/ir.ts:200](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L200) +Defined in: [packages/db/src/query/ir.ts:196](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L196) *** @@ -78,7 +78,7 @@ Defined in: [packages/db/src/query/ir.ts:200](https://github.com/TanStack/db/blo type: "unionAll"; ``` -Defined in: [packages/db/src/query/ir.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L193) +Defined in: [packages/db/src/query/ir.ts:189](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L189) #### Overrides @@ -96,7 +96,7 @@ BaseExpression.type get alias(): string; ``` -Defined in: [packages/db/src/query/ir.ts:204](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L204) +Defined in: [packages/db/src/query/ir.ts:200](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L200) ##### Returns diff --git a/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md b/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md index 914bae09c9..d32971a8ac 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md @@ -3,7 +3,7 @@ id: UnionFrom title: UnionFrom --- -Defined in: [packages/db/src/query/ir.ts:181](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L181) +Defined in: [packages/db/src/query/ir.ts:177](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L177) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:181](https://github.com/TanStack/db/blo new UnionFrom(sources): UnionFrom; ``` -Defined in: [packages/db/src/query/ir.ts:183](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L183) +Defined in: [packages/db/src/query/ir.ts:179](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L179) #### Parameters @@ -45,7 +45,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) **`Internal`** @@ -67,7 +67,7 @@ sources: ( | QueryRef)[]; ``` -Defined in: [packages/db/src/query/ir.ts:183](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L183) +Defined in: [packages/db/src/query/ir.ts:179](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L179) *** @@ -77,7 +77,7 @@ Defined in: [packages/db/src/query/ir.ts:183](https://github.com/TanStack/db/blo type: "unionFrom"; ``` -Defined in: [packages/db/src/query/ir.ts:182](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L182) +Defined in: [packages/db/src/query/ir.ts:178](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L178) #### Overrides @@ -95,7 +95,7 @@ BaseExpression.type get alias(): string; ``` -Defined in: [packages/db/src/query/ir.ts:187](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L187) +Defined in: [packages/db/src/query/ir.ts:183](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L183) ##### Returns diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Value.md b/docs/reference/@tanstack/namespaces/IR/classes/Value.md index cec645caa4..80ebf7e049 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Value.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Value.md @@ -3,7 +3,7 @@ id: Value title: Value --- -Defined in: [packages/db/src/query/ir.ts:245](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L245) +Defined in: [packages/db/src/query/ir.ts:241](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L241) ## Extends @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/ir.ts:245](https://github.com/TanStack/db/blo new Value(value): Value; ``` -Defined in: [packages/db/src/query/ir.ts:247](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L247) +Defined in: [packages/db/src/query/ir.ts:243](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L243) #### Parameters @@ -49,7 +49,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L92) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) **`Internal`** @@ -69,7 +69,7 @@ BaseExpression.__returnType type: "val"; ``` -Defined in: [packages/db/src/query/ir.ts:246](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L246) +Defined in: [packages/db/src/query/ir.ts:242](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L242) #### Overrides @@ -85,4 +85,4 @@ BaseExpression.type value: T; ``` -Defined in: [packages/db/src/query/ir.ts:248](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L248) +Defined in: [packages/db/src/query/ir.ts:244](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L244) diff --git a/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md b/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md index ae39f68b65..312394db9a 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/collectCollectionSources.md @@ -7,7 +7,7 @@ title: collectCollectionSources function collectCollectionSources(query): CollectionRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:397](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L397) +Defined in: [packages/db/src/query/ir.ts:393](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L393) Returns concrete Collection sources after requiring client binding. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md b/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md index 69dbfd1e78..0223bd233b 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/collectPropRefs.md @@ -7,7 +7,7 @@ title: collectPropRefs function collectPropRefs(expression): PropRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:282](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L282) +Defined in: [packages/db/src/query/ir.ts:278](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L278) Collect refs from an IR expression, including aggregate arguments. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md b/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md index 312204d6f6..b834669192 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/collectSourceRefs.md @@ -7,7 +7,7 @@ title: collectSourceRefs function collectSourceRefs(query): CollectionSourceRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:348](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L348) +Defined in: [packages/db/src/query/ir.ts:344](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L344) Returns each lexical Collection or descriptor source in a query tree once. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md b/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md index ecffa6f96d..9742297337 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md @@ -7,7 +7,7 @@ title: createResidualWhere function createResidualWhere(expression): Where; ``` -Defined in: [packages/db/src/query/ir.ts:442](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L442) +Defined in: [packages/db/src/query/ir.ts:438](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L438) Create a residual Where clause from an expression diff --git a/docs/reference/@tanstack/namespaces/IR/functions/followRef.md b/docs/reference/@tanstack/namespaces/IR/functions/followRef.md index 8857836edb..6739108ab0 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/followRef.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/followRef.md @@ -17,7 +17,7 @@ function followRef( }; ``` -Defined in: [packages/db/src/query/ir.ts:490](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L490) +Defined in: [packages/db/src/query/ir.ts:486](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L486) Follows the given reference in a query until its finds the root field the reference points to. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md b/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md index 3b54e2ef80..17460ca42e 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getFromSources.md @@ -9,7 +9,7 @@ function getFromSources(from): ( | QueryRef)[]; ``` -Defined in: [packages/db/src/query/ir.ts:449](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L449) +Defined in: [packages/db/src/query/ir.ts:445](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L445) Sources declared by a FROM clause. UnionAll branches own their sources. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md b/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md index c3e407e73f..fd17036ced 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md @@ -9,7 +9,7 @@ function getHavingExpression(having): | Aggregate; ``` -Defined in: [packages/db/src/query/ir.ts:420](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L420) +Defined in: [packages/db/src/query/ir.ts:416](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L416) Extract the expression from a HAVING clause HAVING clauses can contain aggregates, unlike regular WHERE clauses diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md index 097ec72d56..a6eeadbf3a 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefPropertyPath.md @@ -7,7 +7,7 @@ title: getPropRefPropertyPath function getPropRefPropertyPath(ref): string[]; ``` -Defined in: [packages/db/src/query/ir.ts:241](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L241) +Defined in: [packages/db/src/query/ir.ts:237](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L237) Returns the property path after removing only explicit source qualification. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md index 903557d8aa..b60421c265 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getPropRefSourceAlias.md @@ -7,7 +7,7 @@ title: getPropRefSourceAlias function getPropRefSourceAlias(ref): string | undefined; ``` -Defined in: [packages/db/src/query/ir.ts:234](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L234) +Defined in: [packages/db/src/query/ir.ts:230](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L230) Returns an explicitly declared source alias without inferring from the path. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md b/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md index 1838abc848..83fe4b1efc 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md @@ -7,7 +7,7 @@ title: getWhereExpression function getWhereExpression(where): BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:410](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L410) +Defined in: [packages/db/src/query/ir.ts:406](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L406) Extract the expression from a Where clause diff --git a/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md b/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md index fc18c6038e..7911959e79 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/isBasicOrAggregateExpression.md @@ -7,7 +7,7 @@ title: isBasicOrAggregateExpression function isBasicOrAggregateExpression(value): value is BasicExpression | Aggregate; ``` -Defined in: [packages/db/src/query/ir.ts:328](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L328) +Defined in: [packages/db/src/query/ir.ts:324](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L324) Distinguish compiler expressions from user objects with IR-like fields. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md b/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md index 86e60c410a..d92d563c9d 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md @@ -7,7 +7,7 @@ title: isExpressionLike function isExpressionLike(value): boolean; ``` -Defined in: [packages/db/src/query/ir.ts:339](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L339) +Defined in: [packages/db/src/query/ir.ts:335](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L335) ## Parameters diff --git a/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md b/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md index 00a808222c..740727e4b6 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md @@ -7,7 +7,7 @@ title: isResidualWhere function isResidualWhere(where): boolean; ``` -Defined in: [packages/db/src/query/ir.ts:431](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L431) +Defined in: [packages/db/src/query/ir.ts:427](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L427) Check if a Where clause is marked as residual diff --git a/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md b/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md index 6faaf4f1d0..188fb1bd3e 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/requireCollectionSource.md @@ -9,7 +9,7 @@ title: requireCollectionSource function requireCollectionSource(source): never; ``` -Defined in: [packages/db/src/query/ir.ts:147](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L147) +Defined in: [packages/db/src/query/ir.ts:143](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L143) A descriptor must bind to a DbClient before a Collection is required. @@ -29,7 +29,7 @@ A descriptor must bind to a DbClient before a Collection is required. function requireCollectionSource(source): CollectionImpl; ``` -Defined in: [packages/db/src/query/ir.ts:148](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L148) +Defined in: [packages/db/src/query/ir.ts:144](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L144) A descriptor must bind to a DbClient before a Collection is required. diff --git a/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md b/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md index d6002e6765..72387b2769 100644 --- a/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md +++ b/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md @@ -3,7 +3,7 @@ id: JoinClause title: JoinClause --- -Defined in: [packages/db/src/query/ir.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L54) +Defined in: [packages/db/src/query/ir.ts:53](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L53) ## Properties @@ -15,7 +15,7 @@ from: | QueryRef; ``` -Defined in: [packages/db/src/query/ir.ts:55](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L55) +Defined in: [packages/db/src/query/ir.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L54) *** @@ -25,208 +25,7 @@ Defined in: [packages/db/src/query/ir.ts:55](https://github.com/TanStack/db/blob on: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:57](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L57) - -*** - -### runtime - -```ts -runtime: object; -``` - -Defined in: [packages/db/src/query/ir.ts:60](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L60) - -#### compile() - -```ts -compile: (pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, localFrom, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, valueIdentity, parentKeyStream?) => NamespacedAndKeyedStream = processJoins; -``` - -Processes all join clauses, applying lazy loading optimizations and maintaining -alias tracking for per-alias subscriptions (enables self-joins). - -##### Parameters - -###### pipeline - -[`NamespacedAndKeyedStream`](../../../../type-aliases/NamespacedAndKeyedStream.md) - -###### joinClauses - -`JoinClause`[] - -###### sources - -`Record`\<`string`, [`KeyedStream`](../../../../type-aliases/KeyedStream.md)\> - -###### mainCollectionId - -`string` - -###### mainSource - -`string` - -###### allInputs - -`Record`\<`string`, [`KeyedStream`](../../../../type-aliases/KeyedStream.md)\> - -###### cache - -`QueryCache` - -###### queryMapping - -`QueryMapping` - -###### collections - -`Record`\<`string`, [`Collection`](../../../../interfaces/Collection.md)\> - -###### subscriptions - -`Record`\<`string`, `CollectionSubscription`\> - -###### callbacks - -`Record`\<`string`, `LazyCollectionCallbacks`\> - -###### lazySources - -`Set`\<`string`\> - -###### optimizableOrderByCollections - -`Record`\<`string`, `OrderByOptimizationInfo`\> - -###### setWindowFn - -(`windowFn`) => `void` - -###### rawQuery - -[`QueryIR`](QueryIR.md) - -###### localFrom - -[`From`](../type-aliases/From.md) - -###### onCompileSubquery - -(...`args`) => `CompilationResult` - -###### aliasToCollectionId - -`Record`\<`string`, `string`\> - -###### aliasRemapping - -`Record`\<`string`, `string`\> - -###### sourceWhereClauses - -`Map`\<`string`, [`BasicExpression`](../type-aliases/BasicExpression.md)\<`boolean`\>\> - -###### mainSourceIsParentFiltered - -`boolean` - -###### valueIdentity - -`ValueIdentity` - -###### parentKeyStream? - -[`KeyedStream`](../../../../type-aliases/KeyedStream.md) - -##### Returns - -[`NamespacedAndKeyedStream`](../../../../type-aliases/NamespacedAndKeyedStream.md) - -#### optimize() - -```ts -optimize: (query, groupedClauses) => QueryIR = applyOptimizations; -``` - -Step 4: Apply optimizations by lifting single-source clauses into subqueries. - -Creates a new QueryIR with single-source WHERE clauses moved to subqueries -that wrap the original table references. This ensures immutability and prevents -infinite recursion issues. - -##### Parameters - -###### query - -[`QueryIR`](QueryIR.md) - -Original QueryIR to optimize - -###### groupedClauses - -`GroupedWhereClauses` - -WHERE clauses grouped by optimization strategy - -##### Returns - -[`QueryIR`](QueryIR.md) - -New QueryIR with optimizations applied - -#### reducePublicKeys() - -```ts -reducePublicKeys: (compilation, getKey) => object; -``` - -Reduces a query without includes to one canonical row per public key. - -##### Parameters - -###### compilation - -`CompilationResult` - -###### getKey - -(`row`) => `unknown` - -##### Returns - -`object` - -###### facades - -```ts -facades: undefined; -``` - -###### pipeline - -```ts -pipeline: ResultStream; -``` - -#### validate() - -```ts -validate: (expression) => readonly [BasicExpression, BasicExpression][] = validateJoinConditions; -``` - -Validate join syntax and return its equality operands in source order. - -##### Parameters - -###### expression - -[`BasicExpression`](../type-aliases/BasicExpression.md) - -##### Returns - -readonly \[[`BasicExpression`](../type-aliases/BasicExpression.md), [`BasicExpression`](../type-aliases/BasicExpression.md)\][] +Defined in: [packages/db/src/query/ir.ts:56](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L56) *** @@ -236,4 +35,4 @@ readonly \[[`BasicExpression`](../type-aliases/BasicExpression.md), [`BasicExpre type: "inner" | "left" | "right" | "full" | "outer" | "cross"; ``` -Defined in: [packages/db/src/query/ir.ts:56](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L56) +Defined in: [packages/db/src/query/ir.ts:55](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L55) diff --git a/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md b/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md index 2fabad5f13..f07f8f7bb3 100644 --- a/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md +++ b/docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md @@ -3,7 +3,7 @@ id: QueryIR title: QueryIR --- -Defined in: [packages/db/src/query/ir.ts:17](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L17) +Defined in: [packages/db/src/query/ir.ts:16](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L16) ## Properties @@ -13,7 +13,7 @@ Defined in: [packages/db/src/query/ir.ts:17](https://github.com/TanStack/db/blob optional distinct: true; ``` -Defined in: [packages/db/src/query/ir.ts:29](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L29) +Defined in: [packages/db/src/query/ir.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L28) *** @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/ir.ts:29](https://github.com/TanStack/db/blob optional fnHaving: (row) => any[]; ``` -Defined in: [packages/db/src/query/ir.ts:35](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L35) +Defined in: [packages/db/src/query/ir.ts:34](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L34) #### Parameters @@ -43,7 +43,7 @@ Defined in: [packages/db/src/query/ir.ts:35](https://github.com/TanStack/db/blob optional fnSelect: (row) => any; ``` -Defined in: [packages/db/src/query/ir.ts:33](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L33) +Defined in: [packages/db/src/query/ir.ts:32](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L32) #### Parameters @@ -63,7 +63,7 @@ Defined in: [packages/db/src/query/ir.ts:33](https://github.com/TanStack/db/blob optional fnWhere: (row) => any[]; ``` -Defined in: [packages/db/src/query/ir.ts:34](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L34) +Defined in: [packages/db/src/query/ir.ts:33](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L33) #### Parameters @@ -83,7 +83,7 @@ Defined in: [packages/db/src/query/ir.ts:34](https://github.com/TanStack/db/blob from: From; ``` -Defined in: [packages/db/src/query/ir.ts:18](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L18) +Defined in: [packages/db/src/query/ir.ts:17](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L17) *** @@ -93,7 +93,7 @@ Defined in: [packages/db/src/query/ir.ts:18](https://github.com/TanStack/db/blob optional groupBy: GroupBy; ``` -Defined in: [packages/db/src/query/ir.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L22) +Defined in: [packages/db/src/query/ir.ts:21](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L21) *** @@ -103,7 +103,7 @@ Defined in: [packages/db/src/query/ir.ts:22](https://github.com/TanStack/db/blob optional grouping: object; ``` -Defined in: [packages/db/src/query/ir.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L24) +Defined in: [packages/db/src/query/ir.ts:23](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L23) #### processGroupBy() @@ -210,7 +210,7 @@ A transformed BasicExpression that references computed values instead of raw exp optional having: Where[]; ``` -Defined in: [packages/db/src/query/ir.ts:25](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L25) +Defined in: [packages/db/src/query/ir.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L24) *** @@ -220,7 +220,7 @@ Defined in: [packages/db/src/query/ir.ts:25](https://github.com/TanStack/db/blob optional join: Join; ``` -Defined in: [packages/db/src/query/ir.ts:20](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L20) +Defined in: [packages/db/src/query/ir.ts:19](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L19) *** @@ -230,7 +230,7 @@ Defined in: [packages/db/src/query/ir.ts:20](https://github.com/TanStack/db/blob optional limit: number; ``` -Defined in: [packages/db/src/query/ir.ts:27](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L27) +Defined in: [packages/db/src/query/ir.ts:26](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L26) *** @@ -240,7 +240,7 @@ Defined in: [packages/db/src/query/ir.ts:27](https://github.com/TanStack/db/blob optional offset: number; ``` -Defined in: [packages/db/src/query/ir.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L28) +Defined in: [packages/db/src/query/ir.ts:27](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L27) *** @@ -250,7 +250,7 @@ Defined in: [packages/db/src/query/ir.ts:28](https://github.com/TanStack/db/blob optional orderBy: OrderBy; ``` -Defined in: [packages/db/src/query/ir.ts:26](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L26) +Defined in: [packages/db/src/query/ir.ts:25](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L25) *** @@ -260,7 +260,7 @@ Defined in: [packages/db/src/query/ir.ts:26](https://github.com/TanStack/db/blob optional select: Select; ``` -Defined in: [packages/db/src/query/ir.ts:19](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L19) +Defined in: [packages/db/src/query/ir.ts:18](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L18) *** @@ -270,7 +270,7 @@ Defined in: [packages/db/src/query/ir.ts:19](https://github.com/TanStack/db/blob optional singleResult: true; ``` -Defined in: [packages/db/src/query/ir.ts:30](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L30) +Defined in: [packages/db/src/query/ir.ts:29](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L29) *** @@ -280,4 +280,4 @@ Defined in: [packages/db/src/query/ir.ts:30](https://github.com/TanStack/db/blob optional where: Where[]; ``` -Defined in: [packages/db/src/query/ir.ts:21](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L21) +Defined in: [packages/db/src/query/ir.ts:20](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L20) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md index dda743a2b0..679e081f3d 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md @@ -10,7 +10,7 @@ type BasicExpression = | Func; ``` -Defined in: [packages/db/src/query/ir.ts:267](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L267) +Defined in: [packages/db/src/query/ir.ts:263](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L263) ## Type Parameters diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md index cbe8ce0a66..d53b2f80bb 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/CollectionSourceRef.md @@ -9,4 +9,4 @@ type CollectionSourceRef = | DescriptorRef; ``` -Defined in: [packages/db/src/query/ir.ts:43](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L43) +Defined in: [packages/db/src/query/ir.ts:42](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L42) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md index a0d467b5ec..7efc7e9abe 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md @@ -7,7 +7,7 @@ title: ConditionalSelectBranch type ConditionalSelectBranch = object; ``` -Defined in: [packages/db/src/query/ir.ts:309](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L309) +Defined in: [packages/db/src/query/ir.ts:305](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L305) ## Properties @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:309](https://github.com/TanStack/db/blo condition: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:310](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L310) +Defined in: [packages/db/src/query/ir.ts:306](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L306) *** @@ -27,4 +27,4 @@ Defined in: [packages/db/src/query/ir.ts:310](https://github.com/TanStack/db/blo value: SelectValueExpression; ``` -Defined in: [packages/db/src/query/ir.ts:311](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L311) +Defined in: [packages/db/src/query/ir.ts:307](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L307) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md index b7ae114705..e4b82f44af 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md @@ -11,4 +11,4 @@ type From = | UnionAll; ``` -Defined in: [packages/db/src/query/ir.ts:45](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L45) +Defined in: [packages/db/src/query/ir.ts:44](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L44) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md index 3d56fd3164..688238e51f 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md @@ -7,4 +7,4 @@ title: GroupBy type GroupBy = BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:67](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L67) +Defined in: [packages/db/src/query/ir.ts:63](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L63) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md index 66364095d6..a863bc9713 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md @@ -7,4 +7,4 @@ title: Having type Having = Where; ``` -Defined in: [packages/db/src/query/ir.ts:69](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L69) +Defined in: [packages/db/src/query/ir.ts:65](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L65) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md index 4bbd86b534..b6a441fe38 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md @@ -7,4 +7,4 @@ title: IncludesMaterialization type IncludesMaterialization = "collection" | "array" | "singleton" | "concat"; ``` -Defined in: [packages/db/src/query/ir.ts:38](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L38) +Defined in: [packages/db/src/query/ir.ts:37](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L37) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md index 9be18ed357..2ff4eeeb36 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md @@ -7,4 +7,4 @@ title: Join type Join = JoinClause[]; ``` -Defined in: [packages/db/src/query/ir.ts:52](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L52) +Defined in: [packages/db/src/query/ir.ts:51](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L51) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md index 47931bec6c..babb6937fa 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md @@ -7,4 +7,4 @@ title: Limit type Limit = number; ``` -Defined in: [packages/db/src/query/ir.ts:80](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L80) +Defined in: [packages/db/src/query/ir.ts:76](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L76) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md index 180646548f..a5c929829a 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md @@ -7,4 +7,4 @@ title: Offset type Offset = number; ``` -Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) +Defined in: [packages/db/src/query/ir.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L78) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md index 05c7d71787..92b7027abd 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md @@ -7,4 +7,4 @@ title: OrderBy type OrderBy = OrderByClause[]; ``` -Defined in: [packages/db/src/query/ir.ts:71](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L71) +Defined in: [packages/db/src/query/ir.ts:67](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L67) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md index 357401ccac..34afa49deb 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md @@ -7,7 +7,7 @@ title: OrderByClause type OrderByClause = object; ``` -Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L73) +Defined in: [packages/db/src/query/ir.ts:69](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L69) ## Properties @@ -17,7 +17,7 @@ Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob compareOptions: CompareOptions; ``` -Defined in: [packages/db/src/query/ir.ts:75](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L75) +Defined in: [packages/db/src/query/ir.ts:71](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L71) *** @@ -27,4 +27,4 @@ Defined in: [packages/db/src/query/ir.ts:75](https://github.com/TanStack/db/blob expression: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L74) +Defined in: [packages/db/src/query/ir.ts:70](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L70) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md index 5a8bf7acb6..782f8a54d4 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md @@ -7,4 +7,4 @@ title: OrderByDirection type OrderByDirection = "asc" | "desc"; ``` -Defined in: [packages/db/src/query/ir.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L78) +Defined in: [packages/db/src/query/ir.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L74) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md index 022054e1f9..144beb79ec 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md @@ -7,7 +7,7 @@ title: Select type Select = object; ``` -Defined in: [packages/db/src/query/ir.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L47) +Defined in: [packages/db/src/query/ir.ts:46](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L46) ## Index Signature diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md index ca66f79c98..f432403273 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md @@ -12,4 +12,4 @@ type SelectValueExpression = | ConditionalSelect; ``` -Defined in: [packages/db/src/query/ir.ts:314](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L314) +Defined in: [packages/db/src/query/ir.ts:310](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L310) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md index 9e2be908bd..dbfb16f992 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md @@ -12,4 +12,4 @@ type Where = }; ``` -Defined in: [packages/db/src/query/ir.ts:63](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L63) +Defined in: [packages/db/src/query/ir.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L59) diff --git a/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md b/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md index 091cb99177..97d35abe1d 100644 --- a/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md +++ b/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md @@ -7,4 +7,4 @@ title: INCLUDES_SCALAR_FIELD const INCLUDES_SCALAR_FIELD: "__includes_scalar__"; ``` -Defined in: [packages/db/src/query/ir.ts:41](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L41) +Defined in: [packages/db/src/query/ir.ts:40](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L40) diff --git a/docs/reference/classes/AggregateFunctionNotInSelectError.md b/docs/reference/classes/AggregateFunctionNotInSelectError.md index 3297eaa318..644d175fb2 100644 --- a/docs/reference/classes/AggregateFunctionNotInSelectError.md +++ b/docs/reference/classes/AggregateFunctionNotInSelectError.md @@ -3,7 +3,7 @@ id: AggregateFunctionNotInSelectError title: AggregateFunctionNotInSelectError --- -Defined in: [packages/db/src/errors.ts:997](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L997) +Defined in: [packages/db/src/errors.ts:987](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L987) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:997](https://github.com/TanStack/db/blob/ new AggregateFunctionNotInSelectError(functionName): AggregateFunctionNotInSelectError; ``` -Defined in: [packages/db/src/errors.ts:998](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L998) +Defined in: [packages/db/src/errors.ts:988](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L988) #### Parameters diff --git a/docs/reference/classes/BaseQueryBuilder.md b/docs/reference/classes/BaseQueryBuilder.md index 11bc7c0789..4d637dd525 100644 --- a/docs/reference/classes/BaseQueryBuilder.md +++ b/docs/reference/classes/BaseQueryBuilder.md @@ -49,7 +49,7 @@ Whether the builder may keep `query` without copying it. get fn(): object; ``` -Defined in: [packages/db/src/query/builder/index.ts:874](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L874) +Defined in: [packages/db/src/query/builder/index.ts:965](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L965) Functional variants of the query builder These are imperative function that are called for ery row. @@ -192,7 +192,7 @@ query _bindCollectionSources(resolveCollection): BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:979](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L979) +Defined in: [packages/db/src/query/builder/index.ts:1070](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1070) Bind this query for one consumer. Later descriptor clauses wait for another consumer. @@ -214,7 +214,7 @@ Bind this query for one consumer. Later descriptor clauses wait for another cons _getQuery(): QueryIR; ``` -Defined in: [packages/db/src/query/builder/index.ts:971](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L971) +Defined in: [packages/db/src/query/builder/index.ts:1062](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1062) #### Returns @@ -228,7 +228,7 @@ Defined in: [packages/db/src/query/builder/index.ts:971](https://github.com/TanS distinct(): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:791](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L791) +Defined in: [packages/db/src/query/builder/index.ts:882](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L882) Specify that the query should return distinct rows. Deduplicates rows based on the selected columns. @@ -257,7 +257,7 @@ query findOne(): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:811](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L811) +Defined in: [packages/db/src/query/builder/index.ts:902](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L902) Specify that the query should return a single result @@ -322,13 +322,60 @@ query.from({ activeUsers }) *** +### fullJoin() + +```ts +fullJoin(source, onCallback): QueryBuilder, "full">>; +``` + +Defined in: [packages/db/src/query/builder/index.ts:473](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L473) + +Perform a FULL JOIN with another table or subquery + +#### Type Parameters + +##### TSource + +`TSource` *extends* [`Source`](../type-aliases/Source.md) + +#### Parameters + +##### source + +`TSource` + +An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery + +##### onCallback + +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> + +A function that receives table references and returns the join condition + +#### Returns + +[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`MergeContextWithJoinType`](../type-aliases/MergeContextWithJoinType.md)\<`TContext`, [`SchemaFromSource`](../type-aliases/SchemaFromSource.md)\<`TSource`\>, `"full"`\>\> + +A QueryBuilder with the full joined table available + +#### Example + +```ts +// Full join users with posts +query + .from({ users: usersCollection }) + .fullJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) +``` + +*** + ### groupBy() ```ts groupBy(keys): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:706](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L706) +Defined in: [packages/db/src/query/builder/index.ts:797](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L797) Group rows by one or more columns for aggregation @@ -385,7 +432,7 @@ query having(callback): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:479](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L479) +Defined in: [packages/db/src/query/builder/index.ts:570](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L570) Filter grouped rows based on aggregate conditions @@ -428,13 +475,63 @@ query *** +### innerJoin() + +```ts +innerJoin(source, onCallback): QueryBuilder, "inner">>; +``` + +Defined in: [packages/db/src/query/builder/index.ts:447](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L447) + +Perform an INNER JOIN with another table or subquery + +#### Type Parameters + +##### TSource + +`TSource` *extends* [`Source`](../type-aliases/Source.md) + +#### Parameters + +##### source + +`TSource` + +An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery + +##### onCallback + +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> + +A function that receives table references and returns the join condition + +#### Returns + +[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`MergeContextWithJoinType`](../type-aliases/MergeContextWithJoinType.md)\<`TContext`, [`SchemaFromSource`](../type-aliases/SchemaFromSource.md)\<`TSource`\>, `"inner"`\>\> + +A QueryBuilder with the inner joined table available + +#### Example + +```ts +// Inner join users with posts +query + .from({ users: usersCollection }) + .innerJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) +``` + +*** + ### join() ```ts -join(operator): QueryBuilder, TJoinType>>; +join( + source, + onCallback, +type): QueryBuilder, TJoinType>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:343](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L343) +Defined in: [packages/db/src/query/builder/index.ts:342](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L342) Join another table or subquery to the current query @@ -446,19 +543,27 @@ Join another table or subquery to the current query ##### TJoinType -`TJoinType` *extends* [`JoinType`](../type-aliases/JoinType.md) +`TJoinType` *extends* `"inner"` \| `"left"` \| `"right"` \| `"full"` = `"left"` #### Parameters -##### operator +##### source + +`TSource` + +An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery + +##### onCallback + +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> -[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `TJoinType`\> +A function that receives table references and returns the join condition -A join operator: `leftJoin`, `innerJoin`, `rightJoin`, - `fullJoin`, or `joinOf(type, ...)`. Each takes an object with a single - key-value pair (the alias and a Collection or subquery) and a callback - that returns the join condition. Pass it inline so the callback is typed - from this query. +##### type + +`TJoinType` = `...` + +The type of join: 'inner', 'left', 'right', or 'full' (defaults to 'left') #### Returns @@ -472,17 +577,65 @@ A QueryBuilder with the joined table available // Left join users with posts query .from({ users: usersCollection }) - .join(leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))) + .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) -// Inner join +// Inner join with explicit type query .from({ u: usersCollection }) - .join(innerJoin({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId))) + .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId), 'inner') +``` -// Join type chosen at run time +// Join with a subquery +const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active) query - .from({ u: usersCollection }) - .join(joinOf(type, { p: postsCollection }, ({u, p}) => eq(u.id, p.userId))) + .from({ activeUsers }) + .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId)) + +*** + +### leftJoin() + +```ts +leftJoin(source, onCallback): QueryBuilder, "left">>; +``` + +Defined in: [packages/db/src/query/builder/index.ts:395](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L395) + +Perform a LEFT JOIN with another table or subquery + +#### Type Parameters + +##### TSource + +`TSource` *extends* [`Source`](../type-aliases/Source.md) + +#### Parameters + +##### source + +`TSource` + +An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery + +##### onCallback + +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> + +A function that receives table references and returns the join condition + +#### Returns + +[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`MergeContextWithJoinType`](../type-aliases/MergeContextWithJoinType.md)\<`TContext`, [`SchemaFromSource`](../type-aliases/SchemaFromSource.md)\<`TSource`\>, `"left"`\>\> + +A QueryBuilder with the left joined table available + +#### Example + +```ts +// Left join users with posts +query + .from({ users: usersCollection }) + .leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) ``` *** @@ -493,7 +646,7 @@ query limit(count): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:746](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L746) +Defined in: [packages/db/src/query/builder/index.ts:837](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L837) Limit the number of rows returned by the query `orderBy` is required for `limit` @@ -530,7 +683,7 @@ query offset(count): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:770](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L770) +Defined in: [packages/db/src/query/builder/index.ts:861](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L861) Skip a number of rows before returning results `orderBy` is required for `offset` @@ -568,7 +721,7 @@ query orderBy(callback, options): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:613](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L613) +Defined in: [packages/db/src/query/builder/index.ts:704](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L704) Sort the query results by one or more columns @@ -612,6 +765,53 @@ query *** +### rightJoin() + +```ts +rightJoin(source, onCallback): QueryBuilder, "right">>; +``` + +Defined in: [packages/db/src/query/builder/index.ts:421](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L421) + +Perform a RIGHT JOIN with another table or subquery + +#### Type Parameters + +##### TSource + +`TSource` *extends* [`Source`](../type-aliases/Source.md) + +#### Parameters + +##### source + +`TSource` + +An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery + +##### onCallback + +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> + +A function that receives table references and returns the join condition + +#### Returns + +[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`MergeContextWithJoinType`](../type-aliases/MergeContextWithJoinType.md)\<`TContext`, [`SchemaFromSource`](../type-aliases/SchemaFromSource.md)\<`TSource`\>, `"right"`\>\> + +A QueryBuilder with the right joined table available + +#### Example + +```ts +// Right join users with posts +query + .from({ users: usersCollection }) + .rightJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) +``` + +*** + ### select() #### Call Signature @@ -620,7 +820,7 @@ query select(callback): QueryBuilder>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:545](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L545) +Defined in: [packages/db/src/query/builder/index.ts:636](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L636) Select specific columns or computed values from the query @@ -679,7 +879,7 @@ query select(callback): QueryBuilder>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:550](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L550) +Defined in: [packages/db/src/query/builder/index.ts:641](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L641) Select specific columns or computed values from the query @@ -822,7 +1022,7 @@ query where(callback): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:421](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L421) +Defined in: [packages/db/src/query/builder/index.ts:512](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L512) Filter rows based on a condition diff --git a/docs/reference/classes/CannotCombineEmptyExpressionListError.md b/docs/reference/classes/CannotCombineEmptyExpressionListError.md index 651e7d1a02..47ab410efd 100644 --- a/docs/reference/classes/CannotCombineEmptyExpressionListError.md +++ b/docs/reference/classes/CannotCombineEmptyExpressionListError.md @@ -3,7 +3,7 @@ id: CannotCombineEmptyExpressionListError title: CannotCombineEmptyExpressionListError --- -Defined in: [packages/db/src/errors.ts:1129](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1129) +Defined in: [packages/db/src/errors.ts:1119](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1119) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1129](https://github.com/TanStack/db/blob new CannotCombineEmptyExpressionListError(): CannotCombineEmptyExpressionListError; ``` -Defined in: [packages/db/src/errors.ts:1130](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1130) +Defined in: [packages/db/src/errors.ts:1120](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1120) #### Returns diff --git a/docs/reference/classes/CollectionInputNotFoundError.md b/docs/reference/classes/CollectionInputNotFoundError.md index d5517b4dd4..04f5838204 100644 --- a/docs/reference/classes/CollectionInputNotFoundError.md +++ b/docs/reference/classes/CollectionInputNotFoundError.md @@ -3,7 +3,7 @@ id: CollectionInputNotFoundError title: CollectionInputNotFoundError --- -Defined in: [packages/db/src/errors.ts:796](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L796) +Defined in: [packages/db/src/errors.ts:786](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L786) Error thrown when a collection input stream is not found during query compilation. In self-joins, each alias (e.g., 'employee', 'manager') requires its own input stream. @@ -23,7 +23,7 @@ new CollectionInputNotFoundError( availableKeys?): CollectionInputNotFoundError; ``` -Defined in: [packages/db/src/errors.ts:797](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L797) +Defined in: [packages/db/src/errors.ts:787](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L787) #### Parameters diff --git a/docs/reference/classes/CollectionPreloadAbortedError.md b/docs/reference/classes/CollectionPreloadAbortedError.md index febba48f26..23b34af565 100644 --- a/docs/reference/classes/CollectionPreloadAbortedError.md +++ b/docs/reference/classes/CollectionPreloadAbortedError.md @@ -3,7 +3,7 @@ id: CollectionPreloadAbortedError title: CollectionPreloadAbortedError --- -Defined in: [packages/db/src/errors.ts:1098](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1098) +Defined in: [packages/db/src/errors.ts:1088](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1088) A collection was cleaned up before its initial preload became ready. @@ -19,7 +19,7 @@ A collection was cleaned up before its initial preload became ready. new CollectionPreloadAbortedError(): CollectionPreloadAbortedError; ``` -Defined in: [packages/db/src/errors.ts:1099](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1099) +Defined in: [packages/db/src/errors.ts:1089](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1089) #### Returns diff --git a/docs/reference/classes/DistinctRequiresSelectError.md b/docs/reference/classes/DistinctRequiresSelectError.md index 7bcc5a1c2e..e4cad8c58d 100644 --- a/docs/reference/classes/DistinctRequiresSelectError.md +++ b/docs/reference/classes/DistinctRequiresSelectError.md @@ -3,7 +3,7 @@ id: DistinctRequiresSelectError title: DistinctRequiresSelectError --- -Defined in: [packages/db/src/errors.ts:716](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L716) +Defined in: [packages/db/src/errors.ts:706](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L706) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:716](https://github.com/TanStack/db/blob/ new DistinctRequiresSelectError(): DistinctRequiresSelectError; ``` -Defined in: [packages/db/src/errors.ts:717](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L717) +Defined in: [packages/db/src/errors.ts:707](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L707) #### Returns diff --git a/docs/reference/classes/DuplicateAliasInSubqueryError.md b/docs/reference/classes/DuplicateAliasInSubqueryError.md index 577f6ad714..55c064eebb 100644 --- a/docs/reference/classes/DuplicateAliasInSubqueryError.md +++ b/docs/reference/classes/DuplicateAliasInSubqueryError.md @@ -3,7 +3,7 @@ id: DuplicateAliasInSubqueryError title: DuplicateAliasInSubqueryError --- -Defined in: [packages/db/src/errors.ts:828](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L828) +Defined in: [packages/db/src/errors.ts:818](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L818) Retained for callers that imported this formerly thrown error. @@ -24,7 +24,7 @@ are rejected only within one lexical scope or union branch namespace. new DuplicateAliasInSubqueryError(alias, parentAliases): DuplicateAliasInSubqueryError; ``` -Defined in: [packages/db/src/errors.ts:829](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L829) +Defined in: [packages/db/src/errors.ts:819](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L819) #### Parameters diff --git a/docs/reference/classes/EmptyReferencePathError.md b/docs/reference/classes/EmptyReferencePathError.md index 525dfc92a5..58687903cc 100644 --- a/docs/reference/classes/EmptyReferencePathError.md +++ b/docs/reference/classes/EmptyReferencePathError.md @@ -3,7 +3,7 @@ id: EmptyReferencePathError title: EmptyReferencePathError --- -Defined in: [packages/db/src/errors.ts:861](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L861) +Defined in: [packages/db/src/errors.ts:851](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L851) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:861](https://github.com/TanStack/db/blob/ new EmptyReferencePathError(): EmptyReferencePathError; ``` -Defined in: [packages/db/src/errors.ts:862](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L862) +Defined in: [packages/db/src/errors.ts:852](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L852) #### Returns diff --git a/docs/reference/classes/FnSelectWithGroupByError.md b/docs/reference/classes/FnSelectWithGroupByError.md index f3ff9bca84..ceef68332b 100644 --- a/docs/reference/classes/FnSelectWithGroupByError.md +++ b/docs/reference/classes/FnSelectWithGroupByError.md @@ -3,7 +3,7 @@ id: FnSelectWithGroupByError title: FnSelectWithGroupByError --- -Defined in: [packages/db/src/errors.ts:726](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L726) +Defined in: [packages/db/src/errors.ts:716](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L716) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:726](https://github.com/TanStack/db/blob/ new FnSelectWithGroupByError(): FnSelectWithGroupByError; ``` -Defined in: [packages/db/src/errors.ts:727](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L727) +Defined in: [packages/db/src/errors.ts:717](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L717) #### Returns diff --git a/docs/reference/classes/GroupByError.md b/docs/reference/classes/GroupByError.md index 5c380151c4..7b163aee6d 100644 --- a/docs/reference/classes/GroupByError.md +++ b/docs/reference/classes/GroupByError.md @@ -3,7 +3,7 @@ id: GroupByError title: GroupByError --- -Defined in: [packages/db/src/errors.ts:970](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L970) +Defined in: [packages/db/src/errors.ts:960](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L960) ## Extends @@ -24,7 +24,7 @@ Defined in: [packages/db/src/errors.ts:970](https://github.com/TanStack/db/blob/ new GroupByError(message): GroupByError; ``` -Defined in: [packages/db/src/errors.ts:971](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L971) +Defined in: [packages/db/src/errors.ts:961](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L961) #### Parameters diff --git a/docs/reference/classes/GroupingOperatorRequiredError.md b/docs/reference/classes/GroupingOperatorRequiredError.md index 6a720ceeb5..dafe9758cb 100644 --- a/docs/reference/classes/GroupingOperatorRequiredError.md +++ b/docs/reference/classes/GroupingOperatorRequiredError.md @@ -3,7 +3,7 @@ id: GroupingOperatorRequiredError title: GroupingOperatorRequiredError --- -Defined in: [packages/db/src/errors.ts:739](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L739) +Defined in: [packages/db/src/errors.ts:729](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L729) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:739](https://github.com/TanStack/db/blob/ new GroupingOperatorRequiredError(): GroupingOperatorRequiredError; ``` -Defined in: [packages/db/src/errors.ts:740](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L740) +Defined in: [packages/db/src/errors.ts:730](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L730) #### Returns diff --git a/docs/reference/classes/HavingRequiresGroupByError.md b/docs/reference/classes/HavingRequiresGroupByError.md index 76fc96eecc..a707575a7c 100644 --- a/docs/reference/classes/HavingRequiresGroupByError.md +++ b/docs/reference/classes/HavingRequiresGroupByError.md @@ -3,7 +3,7 @@ id: HavingRequiresGroupByError title: HavingRequiresGroupByError --- -Defined in: [packages/db/src/errors.ts:772](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L772) +Defined in: [packages/db/src/errors.ts:762](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L762) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:772](https://github.com/TanStack/db/blob/ new HavingRequiresGroupByError(): HavingRequiresGroupByError; ``` -Defined in: [packages/db/src/errors.ts:773](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L773) +Defined in: [packages/db/src/errors.ts:763](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L763) #### Returns diff --git a/docs/reference/classes/InvalidJoinCondition.md b/docs/reference/classes/InvalidJoinCondition.md index 11e655ea21..833feabca2 100644 --- a/docs/reference/classes/InvalidJoinCondition.md +++ b/docs/reference/classes/InvalidJoinCondition.md @@ -3,7 +3,7 @@ id: InvalidJoinCondition title: InvalidJoinCondition --- -Defined in: [packages/db/src/errors.ts:949](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L949) +Defined in: [packages/db/src/errors.ts:939](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L939) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:949](https://github.com/TanStack/db/blob/ new InvalidJoinCondition(): InvalidJoinCondition; ``` -Defined in: [packages/db/src/errors.ts:950](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L950) +Defined in: [packages/db/src/errors.ts:940](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L940) #### Returns diff --git a/docs/reference/classes/InvalidJoinConditionLeftSourceError.md b/docs/reference/classes/InvalidJoinConditionLeftSourceError.md index 2e737587e5..84eb8b3242 100644 --- a/docs/reference/classes/InvalidJoinConditionLeftSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionLeftSourceError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionLeftSourceError title: InvalidJoinConditionLeftSourceError --- -Defined in: [packages/db/src/errors.ts:929](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L929) +Defined in: [packages/db/src/errors.ts:919](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L919) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:929](https://github.com/TanStack/db/blob/ new InvalidJoinConditionLeftSourceError(sourceAlias): InvalidJoinConditionLeftSourceError; ``` -Defined in: [packages/db/src/errors.ts:930](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L930) +Defined in: [packages/db/src/errors.ts:920](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L920) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionRightSourceError.md b/docs/reference/classes/InvalidJoinConditionRightSourceError.md index c8cd77219a..d334af511a 100644 --- a/docs/reference/classes/InvalidJoinConditionRightSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionRightSourceError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionRightSourceError title: InvalidJoinConditionRightSourceError --- -Defined in: [packages/db/src/errors.ts:939](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L939) +Defined in: [packages/db/src/errors.ts:929](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L929) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:939](https://github.com/TanStack/db/blob/ new InvalidJoinConditionRightSourceError(sourceAlias): InvalidJoinConditionRightSourceError; ``` -Defined in: [packages/db/src/errors.ts:940](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L940) +Defined in: [packages/db/src/errors.ts:930](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L930) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionSameSourceError.md b/docs/reference/classes/InvalidJoinConditionSameSourceError.md index 0cda862de5..cd03e45a53 100644 --- a/docs/reference/classes/InvalidJoinConditionSameSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionSameSourceError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionSameSourceError title: InvalidJoinConditionSameSourceError --- -Defined in: [packages/db/src/errors.ts:909](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L909) +Defined in: [packages/db/src/errors.ts:899](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L899) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:909](https://github.com/TanStack/db/blob/ new InvalidJoinConditionSameSourceError(sourceAlias): InvalidJoinConditionSameSourceError; ``` -Defined in: [packages/db/src/errors.ts:910](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L910) +Defined in: [packages/db/src/errors.ts:900](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L900) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md b/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md index c3a0f8d030..14c8f7dd9b 100644 --- a/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md +++ b/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionSourceMismatchError title: InvalidJoinConditionSourceMismatchError --- -Defined in: [packages/db/src/errors.ts:919](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L919) +Defined in: [packages/db/src/errors.ts:909](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L909) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:919](https://github.com/TanStack/db/blob/ new InvalidJoinConditionSourceMismatchError(): InvalidJoinConditionSourceMismatchError; ``` -Defined in: [packages/db/src/errors.ts:920](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L920) +Defined in: [packages/db/src/errors.ts:910](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L910) #### Returns diff --git a/docs/reference/classes/InvalidSourceTypeError.md b/docs/reference/classes/InvalidSourceTypeError.md index fd06e456bf..cff1999971 100644 --- a/docs/reference/classes/InvalidSourceTypeError.md +++ b/docs/reference/classes/InvalidSourceTypeError.md @@ -3,7 +3,7 @@ id: InvalidSourceTypeError title: InvalidSourceTypeError --- -Defined in: [packages/db/src/errors.ts:636](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L636) +Defined in: [packages/db/src/errors.ts:626](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L626) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:636](https://github.com/TanStack/db/blob/ new InvalidSourceTypeError(context, type): InvalidSourceTypeError; ``` -Defined in: [packages/db/src/errors.ts:637](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L637) +Defined in: [packages/db/src/errors.ts:627](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L627) #### Parameters diff --git a/docs/reference/classes/InvalidStorageDataFormatError.md b/docs/reference/classes/InvalidStorageDataFormatError.md index 02cc96b418..78e9e71e62 100644 --- a/docs/reference/classes/InvalidStorageDataFormatError.md +++ b/docs/reference/classes/InvalidStorageDataFormatError.md @@ -3,7 +3,7 @@ id: InvalidStorageDataFormatError title: InvalidStorageDataFormatError --- -Defined in: [packages/db/src/errors.ts:1053](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1053) +Defined in: [packages/db/src/errors.ts:1043](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1043) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1053](https://github.com/TanStack/db/blob new InvalidStorageDataFormatError(storageKey, key): InvalidStorageDataFormatError; ``` -Defined in: [packages/db/src/errors.ts:1054](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1054) +Defined in: [packages/db/src/errors.ts:1044](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1044) #### Parameters diff --git a/docs/reference/classes/InvalidStorageObjectFormatError.md b/docs/reference/classes/InvalidStorageObjectFormatError.md index 641af94173..dc8b39fc16 100644 --- a/docs/reference/classes/InvalidStorageObjectFormatError.md +++ b/docs/reference/classes/InvalidStorageObjectFormatError.md @@ -3,7 +3,7 @@ id: InvalidStorageObjectFormatError title: InvalidStorageObjectFormatError --- -Defined in: [packages/db/src/errors.ts:1063](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1063) +Defined in: [packages/db/src/errors.ts:1053](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1053) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1063](https://github.com/TanStack/db/blob new InvalidStorageObjectFormatError(storageKey): InvalidStorageObjectFormatError; ``` -Defined in: [packages/db/src/errors.ts:1064](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1064) +Defined in: [packages/db/src/errors.ts:1054](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1054) #### Parameters diff --git a/docs/reference/classes/InvalidWhereExpressionError.md b/docs/reference/classes/InvalidWhereExpressionError.md index b1e5349e40..e87dd6b535 100644 --- a/docs/reference/classes/InvalidWhereExpressionError.md +++ b/docs/reference/classes/InvalidWhereExpressionError.md @@ -3,7 +3,7 @@ id: InvalidWhereExpressionError title: InvalidWhereExpressionError --- -Defined in: [packages/db/src/errors.ts:681](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L681) +Defined in: [packages/db/src/errors.ts:671](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L671) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:681](https://github.com/TanStack/db/blob/ new InvalidWhereExpressionError(valueType): InvalidWhereExpressionError; ``` -Defined in: [packages/db/src/errors.ts:682](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L682) +Defined in: [packages/db/src/errors.ts:672](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L672) #### Parameters diff --git a/docs/reference/classes/JoinCollectionNotFoundError.md b/docs/reference/classes/JoinCollectionNotFoundError.md index 45f63eb577..8ea45dc86b 100644 --- a/docs/reference/classes/JoinCollectionNotFoundError.md +++ b/docs/reference/classes/JoinCollectionNotFoundError.md @@ -3,7 +3,7 @@ id: JoinCollectionNotFoundError title: JoinCollectionNotFoundError --- -Defined in: [packages/db/src/errors.ts:881](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L881) +Defined in: [packages/db/src/errors.ts:871](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L871) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:881](https://github.com/TanStack/db/blob/ new JoinCollectionNotFoundError(collectionId): JoinCollectionNotFoundError; ``` -Defined in: [packages/db/src/errors.ts:882](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L882) +Defined in: [packages/db/src/errors.ts:872](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L872) #### Parameters diff --git a/docs/reference/classes/JoinConditionMustBeEqualityError.md b/docs/reference/classes/JoinConditionMustBeEqualityError.md index 4f87f7ab57..091696c438 100644 --- a/docs/reference/classes/JoinConditionMustBeEqualityError.md +++ b/docs/reference/classes/JoinConditionMustBeEqualityError.md @@ -3,7 +3,7 @@ id: JoinConditionMustBeEqualityError title: JoinConditionMustBeEqualityError --- -Defined in: [packages/db/src/errors.ts:661](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L661) +Defined in: [packages/db/src/errors.ts:651](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L651) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:661](https://github.com/TanStack/db/blob/ new JoinConditionMustBeEqualityError(): JoinConditionMustBeEqualityError; ``` -Defined in: [packages/db/src/errors.ts:662](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L662) +Defined in: [packages/db/src/errors.ts:652](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L652) #### Returns diff --git a/docs/reference/classes/JoinError.md b/docs/reference/classes/JoinError.md index 67a9ad1c47..750e4fc653 100644 --- a/docs/reference/classes/JoinError.md +++ b/docs/reference/classes/JoinError.md @@ -3,7 +3,7 @@ id: JoinError title: JoinError --- -Defined in: [packages/db/src/errors.ts:892](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L892) +Defined in: [packages/db/src/errors.ts:882](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L882) ## Extends @@ -27,7 +27,7 @@ Defined in: [packages/db/src/errors.ts:892](https://github.com/TanStack/db/blob/ new JoinError(message): JoinError; ``` -Defined in: [packages/db/src/errors.ts:893](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L893) +Defined in: [packages/db/src/errors.ts:883](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L883) #### Parameters diff --git a/docs/reference/classes/JoinOperatorRequiredError.md b/docs/reference/classes/JoinOperatorRequiredError.md deleted file mode 100644 index a87145a8b8..0000000000 --- a/docs/reference/classes/JoinOperatorRequiredError.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -id: JoinOperatorRequiredError -title: JoinOperatorRequiredError ---- - -Defined in: [packages/db/src/errors.ts:623](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L623) - -## Extends - -- [`QueryBuilderError`](QueryBuilderError.md) - -## Constructors - -### Constructor - -```ts -new JoinOperatorRequiredError(type): JoinOperatorRequiredError; -``` - -Defined in: [packages/db/src/errors.ts:624](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L624) - -#### Parameters - -##### type - -`string` - -#### Returns - -`JoinOperatorRequiredError` - -#### Overrides - -[`QueryBuilderError`](QueryBuilderError.md).[`constructor`](QueryBuilderError.md#constructor) - -## Properties - -### cause? - -```ts -optional cause: unknown; -``` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:26 - -#### Inherited from - -[`QueryBuilderError`](QueryBuilderError.md).[`cause`](QueryBuilderError.md#cause) - -*** - -### message - -```ts -message: string; -``` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1077 - -#### Inherited from - -[`QueryBuilderError`](QueryBuilderError.md).[`message`](QueryBuilderError.md#message) - -*** - -### name - -```ts -name: string; -``` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 - -#### Inherited from - -[`QueryBuilderError`](QueryBuilderError.md).[`name`](QueryBuilderError.md#name) - -*** - -### stack? - -```ts -optional stack: string; -``` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1078 - -#### Inherited from - -[`QueryBuilderError`](QueryBuilderError.md).[`stack`](QueryBuilderError.md#stack) - -*** - -### stackTraceLimit - -```ts -static stackTraceLimit: number; -``` - -Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:67 - -The `Error.stackTraceLimit` property specifies the number of stack frames -collected by a stack trace (whether generated by `new Error().stack` or -`Error.captureStackTrace(obj)`). - -The default value is `10` but may be set to any valid JavaScript number. Changes -will affect any stack trace captured _after_ the value has been changed. - -If set to a non-number value, or set to a negative number, stack traces will -not capture any frames. - -#### Inherited from - -[`QueryBuilderError`](QueryBuilderError.md).[`stackTraceLimit`](QueryBuilderError.md#stacktracelimit) - -## Methods - -### captureStackTrace() - -```ts -static captureStackTrace(targetObject, constructorOpt?): void; -``` - -Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:51 - -Creates a `.stack` property on `targetObject`, which when accessed returns -a string representing the location in the code at which -`Error.captureStackTrace()` was called. - -```js -const myObject = {}; -Error.captureStackTrace(myObject); -myObject.stack; // Similar to `new Error().stack` -``` - -The first line of the trace will be prefixed with -`${myObject.name}: ${myObject.message}`. - -The optional `constructorOpt` argument accepts a function. If given, all frames -above `constructorOpt`, including `constructorOpt`, will be omitted from the -generated stack trace. - -The `constructorOpt` argument is useful for hiding implementation -details of error generation from the user. For instance: - -```js -function a() { - b(); -} - -function b() { - c(); -} - -function c() { - // Create an error without stack trace to avoid calculating the stack trace twice. - const { stackTraceLimit } = Error; - Error.stackTraceLimit = 0; - const error = new Error(); - Error.stackTraceLimit = stackTraceLimit; - - // Capture the stack trace above function b - Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace - throw error; -} - -a(); -``` - -#### Parameters - -##### targetObject - -`object` - -##### constructorOpt? - -`Function` - -#### Returns - -`void` - -#### Inherited from - -[`QueryBuilderError`](QueryBuilderError.md).[`captureStackTrace`](QueryBuilderError.md#capturestacktrace) - -*** - -### prepareStackTrace() - -```ts -static prepareStackTrace(err, stackTraces): any; -``` - -Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:55 - -#### Parameters - -##### err - -`Error` - -##### stackTraces - -`CallSite`[] - -#### Returns - -`any` - -#### See - -https://v8.dev/docs/stack-trace-api#customizing-stack-traces - -#### Inherited from - -[`QueryBuilderError`](QueryBuilderError.md).[`prepareStackTrace`](QueryBuilderError.md#preparestacktrace) diff --git a/docs/reference/classes/LimitOffsetRequireOrderByError.md b/docs/reference/classes/LimitOffsetRequireOrderByError.md index 8c51ac8b7b..1e0b4ec6cd 100644 --- a/docs/reference/classes/LimitOffsetRequireOrderByError.md +++ b/docs/reference/classes/LimitOffsetRequireOrderByError.md @@ -3,7 +3,7 @@ id: LimitOffsetRequireOrderByError title: LimitOffsetRequireOrderByError --- -Defined in: [packages/db/src/errors.ts:782](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L782) +Defined in: [packages/db/src/errors.ts:772](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L772) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:782](https://github.com/TanStack/db/blob/ new LimitOffsetRequireOrderByError(): LimitOffsetRequireOrderByError; ``` -Defined in: [packages/db/src/errors.ts:783](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L783) +Defined in: [packages/db/src/errors.ts:773](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L773) #### Returns diff --git a/docs/reference/classes/LoadSubsetOperationAbortedError.md b/docs/reference/classes/LoadSubsetOperationAbortedError.md index b65e1a933b..3835561a45 100644 --- a/docs/reference/classes/LoadSubsetOperationAbortedError.md +++ b/docs/reference/classes/LoadSubsetOperationAbortedError.md @@ -3,7 +3,7 @@ id: LoadSubsetOperationAbortedError title: LoadSubsetOperationAbortedError --- -Defined in: [packages/db/src/errors.ts:1110](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1110) +Defined in: [packages/db/src/errors.ts:1100](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1100) A subset operation was canceled before its result became visible. @@ -19,7 +19,7 @@ A subset operation was canceled before its result became visible. new LoadSubsetOperationAbortedError(): LoadSubsetOperationAbortedError; ``` -Defined in: [packages/db/src/errors.ts:1111](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1111) +Defined in: [packages/db/src/errors.ts:1101](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1101) #### Returns diff --git a/docs/reference/classes/LocalStorageCollectionError.md b/docs/reference/classes/LocalStorageCollectionError.md index 3a34169cdf..79f4f2209e 100644 --- a/docs/reference/classes/LocalStorageCollectionError.md +++ b/docs/reference/classes/LocalStorageCollectionError.md @@ -3,7 +3,7 @@ id: LocalStorageCollectionError title: LocalStorageCollectionError --- -Defined in: [packages/db/src/errors.ts:1036](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1036) +Defined in: [packages/db/src/errors.ts:1026](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1026) ## Extends @@ -23,7 +23,7 @@ Defined in: [packages/db/src/errors.ts:1036](https://github.com/TanStack/db/blob new LocalStorageCollectionError(message): LocalStorageCollectionError; ``` -Defined in: [packages/db/src/errors.ts:1037](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1037) +Defined in: [packages/db/src/errors.ts:1027](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1027) #### Parameters diff --git a/docs/reference/classes/MissingAliasInputsError.md b/docs/reference/classes/MissingAliasInputsError.md index 80fa5052b6..c426bf423d 100644 --- a/docs/reference/classes/MissingAliasInputsError.md +++ b/docs/reference/classes/MissingAliasInputsError.md @@ -3,7 +3,7 @@ id: MissingAliasInputsError title: MissingAliasInputsError --- -Defined in: [packages/db/src/errors.ts:1143](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1143) +Defined in: [packages/db/src/errors.ts:1133](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1133) Internal error when the compiler returns aliases that don't have corresponding input streams. This should never happen since all aliases come from user declarations. @@ -20,7 +20,7 @@ This should never happen since all aliases come from user declarations. new MissingAliasInputsError(missingAliases): MissingAliasInputsError; ``` -Defined in: [packages/db/src/errors.ts:1144](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1144) +Defined in: [packages/db/src/errors.ts:1134](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1134) #### Parameters diff --git a/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md b/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md index 3005f35446..122cd0987e 100644 --- a/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md +++ b/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md @@ -3,7 +3,7 @@ id: NonAggregateExpressionNotInGroupByError title: NonAggregateExpressionNotInGroupByError --- -Defined in: [packages/db/src/errors.ts:977](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L977) +Defined in: [packages/db/src/errors.ts:967](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L967) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:977](https://github.com/TanStack/db/blob/ new NonAggregateExpressionNotInGroupByError(alias): NonAggregateExpressionNotInGroupByError; ``` -Defined in: [packages/db/src/errors.ts:978](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L978) +Defined in: [packages/db/src/errors.ts:968](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L968) #### Parameters diff --git a/docs/reference/classes/QueryBuilderError.md b/docs/reference/classes/QueryBuilderError.md index cc05f01b77..e6c6a58429 100644 --- a/docs/reference/classes/QueryBuilderError.md +++ b/docs/reference/classes/QueryBuilderError.md @@ -15,7 +15,6 @@ Defined in: [packages/db/src/errors.ts:576](https://github.com/TanStack/db/blob/ - [`SubQueryMustHaveFromClauseError`](SubQueryMustHaveFromClauseError.md) - [`InvalidSourceError`](InvalidSourceError.md) - [`IncludeOperatorRequiredError`](IncludeOperatorRequiredError.md) -- [`JoinOperatorRequiredError`](JoinOperatorRequiredError.md) - [`InvalidSourceTypeError`](InvalidSourceTypeError.md) - [`JoinConditionMustBeEqualityError`](JoinConditionMustBeEqualityError.md) - [`QueryMustHaveFromClauseError`](QueryMustHaveFromClauseError.md) diff --git a/docs/reference/classes/QueryCompilationError.md b/docs/reference/classes/QueryCompilationError.md index efc8fbd06a..d41977d8ea 100644 --- a/docs/reference/classes/QueryCompilationError.md +++ b/docs/reference/classes/QueryCompilationError.md @@ -3,7 +3,7 @@ id: QueryCompilationError title: QueryCompilationError --- -Defined in: [packages/db/src/errors.ts:697](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L697) +Defined in: [packages/db/src/errors.ts:687](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L687) ## Extends @@ -38,7 +38,7 @@ Defined in: [packages/db/src/errors.ts:697](https://github.com/TanStack/db/blob/ new QueryCompilationError(message): QueryCompilationError; ``` -Defined in: [packages/db/src/errors.ts:698](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L698) +Defined in: [packages/db/src/errors.ts:688](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L688) #### Parameters diff --git a/docs/reference/classes/QueryMustHaveFromClauseError.md b/docs/reference/classes/QueryMustHaveFromClauseError.md index 41f5655047..3118c5f841 100644 --- a/docs/reference/classes/QueryMustHaveFromClauseError.md +++ b/docs/reference/classes/QueryMustHaveFromClauseError.md @@ -3,7 +3,7 @@ id: QueryMustHaveFromClauseError title: QueryMustHaveFromClauseError --- -Defined in: [packages/db/src/errors.ts:671](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L671) +Defined in: [packages/db/src/errors.ts:661](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L661) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:671](https://github.com/TanStack/db/blob/ new QueryMustHaveFromClauseError(): QueryMustHaveFromClauseError; ``` -Defined in: [packages/db/src/errors.ts:672](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L672) +Defined in: [packages/db/src/errors.ts:662](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L662) #### Returns diff --git a/docs/reference/classes/QueryOptimizerError.md b/docs/reference/classes/QueryOptimizerError.md index a698c592ce..1056285c53 100644 --- a/docs/reference/classes/QueryOptimizerError.md +++ b/docs/reference/classes/QueryOptimizerError.md @@ -3,7 +3,7 @@ id: QueryOptimizerError title: QueryOptimizerError --- -Defined in: [packages/db/src/errors.ts:1122](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1122) +Defined in: [packages/db/src/errors.ts:1112](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1112) ## Extends @@ -21,7 +21,7 @@ Defined in: [packages/db/src/errors.ts:1122](https://github.com/TanStack/db/blob new QueryOptimizerError(message): QueryOptimizerError; ``` -Defined in: [packages/db/src/errors.ts:1123](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1123) +Defined in: [packages/db/src/errors.ts:1113](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1113) #### Parameters diff --git a/docs/reference/classes/SerializationError.md b/docs/reference/classes/SerializationError.md index ec30df6fbe..5316cec026 100644 --- a/docs/reference/classes/SerializationError.md +++ b/docs/reference/classes/SerializationError.md @@ -3,7 +3,7 @@ id: SerializationError title: SerializationError --- -Defined in: [packages/db/src/errors.ts:1025](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1025) +Defined in: [packages/db/src/errors.ts:1015](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1015) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1025](https://github.com/TanStack/db/blob new SerializationError(operation, originalError): SerializationError; ``` -Defined in: [packages/db/src/errors.ts:1026](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1026) +Defined in: [packages/db/src/errors.ts:1016](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1016) #### Parameters diff --git a/docs/reference/classes/SetWindowFindOneLimitError.md b/docs/reference/classes/SetWindowFindOneLimitError.md index 4eb3180e58..ac85da1cf6 100644 --- a/docs/reference/classes/SetWindowFindOneLimitError.md +++ b/docs/reference/classes/SetWindowFindOneLimitError.md @@ -3,7 +3,7 @@ id: SetWindowFindOneLimitError title: SetWindowFindOneLimitError --- -Defined in: [packages/db/src/errors.ts:1171](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1171) +Defined in: [packages/db/src/errors.ts:1161](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1161) Error thrown when setWindow would change the limit of a findOne() query. @@ -19,7 +19,7 @@ Error thrown when setWindow would change the limit of a findOne() query. new SetWindowFindOneLimitError(): SetWindowFindOneLimitError; ``` -Defined in: [packages/db/src/errors.ts:1172](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1172) +Defined in: [packages/db/src/errors.ts:1162](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1162) #### Returns diff --git a/docs/reference/classes/SetWindowReentrancyError.md b/docs/reference/classes/SetWindowReentrancyError.md index 863fd852f6..1d8c498eb4 100644 --- a/docs/reference/classes/SetWindowReentrancyError.md +++ b/docs/reference/classes/SetWindowReentrancyError.md @@ -3,7 +3,7 @@ id: SetWindowReentrancyError title: SetWindowReentrancyError --- -Defined in: [packages/db/src/errors.ts:1182](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1182) +Defined in: [packages/db/src/errors.ts:1172](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1172) Error thrown when setWindow is called from inside another setWindow call. @@ -19,7 +19,7 @@ Error thrown when setWindow is called from inside another setWindow call. new SetWindowReentrancyError(): SetWindowReentrancyError; ``` -Defined in: [packages/db/src/errors.ts:1183](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1183) +Defined in: [packages/db/src/errors.ts:1173](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1173) #### Returns diff --git a/docs/reference/classes/SetWindowRequiresOrderByError.md b/docs/reference/classes/SetWindowRequiresOrderByError.md index 1cfa34eedd..cbf09e17f9 100644 --- a/docs/reference/classes/SetWindowRequiresOrderByError.md +++ b/docs/reference/classes/SetWindowRequiresOrderByError.md @@ -3,7 +3,7 @@ id: SetWindowRequiresOrderByError title: SetWindowRequiresOrderByError --- -Defined in: [packages/db/src/errors.ts:1157](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1157) +Defined in: [packages/db/src/errors.ts:1147](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1147) Error thrown when setWindow is called on a collection without an ORDER BY clause. @@ -19,7 +19,7 @@ Error thrown when setWindow is called on a collection without an ORDER BY clause new SetWindowRequiresOrderByError(): SetWindowRequiresOrderByError; ``` -Defined in: [packages/db/src/errors.ts:1158](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1158) +Defined in: [packages/db/src/errors.ts:1148](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1148) #### Returns diff --git a/docs/reference/classes/StorageError.md b/docs/reference/classes/StorageError.md index b27a79ecb2..94654fc200 100644 --- a/docs/reference/classes/StorageError.md +++ b/docs/reference/classes/StorageError.md @@ -3,7 +3,7 @@ id: StorageError title: StorageError --- -Defined in: [packages/db/src/errors.ts:1018](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1018) +Defined in: [packages/db/src/errors.ts:1008](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1008) ## Extends @@ -22,7 +22,7 @@ Defined in: [packages/db/src/errors.ts:1018](https://github.com/TanStack/db/blob new StorageError(message): StorageError; ``` -Defined in: [packages/db/src/errors.ts:1019](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1019) +Defined in: [packages/db/src/errors.ts:1009](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1009) #### Parameters diff --git a/docs/reference/classes/StorageKeyRequiredError.md b/docs/reference/classes/StorageKeyRequiredError.md index 8a117370f2..64de2c736d 100644 --- a/docs/reference/classes/StorageKeyRequiredError.md +++ b/docs/reference/classes/StorageKeyRequiredError.md @@ -3,7 +3,7 @@ id: StorageKeyRequiredError title: StorageKeyRequiredError --- -Defined in: [packages/db/src/errors.ts:1043](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1043) +Defined in: [packages/db/src/errors.ts:1033](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1033) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1043](https://github.com/TanStack/db/blob new StorageKeyRequiredError(): StorageKeyRequiredError; ``` -Defined in: [packages/db/src/errors.ts:1044](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1044) +Defined in: [packages/db/src/errors.ts:1034](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1034) #### Returns diff --git a/docs/reference/classes/SyncCleanupError.md b/docs/reference/classes/SyncCleanupError.md index efaa54d67f..5d695bdbb9 100644 --- a/docs/reference/classes/SyncCleanupError.md +++ b/docs/reference/classes/SyncCleanupError.md @@ -3,7 +3,7 @@ id: SyncCleanupError title: SyncCleanupError --- -Defined in: [packages/db/src/errors.ts:1074](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1074) +Defined in: [packages/db/src/errors.ts:1064](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1064) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1074](https://github.com/TanStack/db/blob new SyncCleanupError(collectionId, error): SyncCleanupError; ``` -Defined in: [packages/db/src/errors.ts:1075](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1075) +Defined in: [packages/db/src/errors.ts:1065](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1065) #### Parameters diff --git a/docs/reference/classes/SyncTransactionAbortedError.md b/docs/reference/classes/SyncTransactionAbortedError.md index 77815f83e3..952aaf9f86 100644 --- a/docs/reference/classes/SyncTransactionAbortedError.md +++ b/docs/reference/classes/SyncTransactionAbortedError.md @@ -3,7 +3,7 @@ id: SyncTransactionAbortedError title: SyncTransactionAbortedError --- -Defined in: [packages/db/src/errors.ts:1086](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1086) +Defined in: [packages/db/src/errors.ts:1076](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1076) A sync transaction was canceled before its writes became visible. @@ -19,7 +19,7 @@ A sync transaction was canceled before its writes became visible. new SyncTransactionAbortedError(): SyncTransactionAbortedError; ``` -Defined in: [packages/db/src/errors.ts:1087](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1087) +Defined in: [packages/db/src/errors.ts:1077](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1077) #### Returns diff --git a/docs/reference/classes/UnhashableQueryIRError.md b/docs/reference/classes/UnhashableQueryIRError.md index a6a4958970..9d1cb7c815 100644 --- a/docs/reference/classes/UnhashableQueryIRError.md +++ b/docs/reference/classes/UnhashableQueryIRError.md @@ -3,7 +3,7 @@ id: UnhashableQueryIRError title: UnhashableQueryIRError --- -Defined in: [packages/db/src/errors.ts:1193](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1193) +Defined in: [packages/db/src/errors.ts:1183](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1183) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1193](https://github.com/TanStack/db/blob new UnhashableQueryIRError(path, reason): UnhashableQueryIRError; ``` -Defined in: [packages/db/src/errors.ts:1194](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1194) +Defined in: [packages/db/src/errors.ts:1184](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1184) #### Parameters @@ -95,7 +95,7 @@ Error.name readonly path: string; ``` -Defined in: [packages/db/src/errors.ts:1195](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1195) +Defined in: [packages/db/src/errors.ts:1185](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1185) *** @@ -105,7 +105,7 @@ Defined in: [packages/db/src/errors.ts:1195](https://github.com/TanStack/db/blob readonly reason: string; ``` -Defined in: [packages/db/src/errors.ts:1196](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1196) +Defined in: [packages/db/src/errors.ts:1186](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1186) *** diff --git a/docs/reference/classes/UnknownExpressionTypeError.md b/docs/reference/classes/UnknownExpressionTypeError.md index 520f60adf3..598f0a73ee 100644 --- a/docs/reference/classes/UnknownExpressionTypeError.md +++ b/docs/reference/classes/UnknownExpressionTypeError.md @@ -3,7 +3,7 @@ id: UnknownExpressionTypeError title: UnknownExpressionTypeError --- -Defined in: [packages/db/src/errors.ts:851](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L851) +Defined in: [packages/db/src/errors.ts:841](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L841) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:851](https://github.com/TanStack/db/blob/ new UnknownExpressionTypeError(type): UnknownExpressionTypeError; ``` -Defined in: [packages/db/src/errors.ts:852](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L852) +Defined in: [packages/db/src/errors.ts:842](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L842) #### Parameters diff --git a/docs/reference/classes/UnknownFunctionError.md b/docs/reference/classes/UnknownFunctionError.md index 5e80b672c1..86133140b9 100644 --- a/docs/reference/classes/UnknownFunctionError.md +++ b/docs/reference/classes/UnknownFunctionError.md @@ -3,7 +3,7 @@ id: UnknownFunctionError title: UnknownFunctionError --- -Defined in: [packages/db/src/errors.ts:871](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L871) +Defined in: [packages/db/src/errors.ts:861](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L861) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:871](https://github.com/TanStack/db/blob/ new UnknownFunctionError(functionName): UnknownFunctionError; ``` -Defined in: [packages/db/src/errors.ts:872](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L872) +Defined in: [packages/db/src/errors.ts:862](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L862) #### Parameters diff --git a/docs/reference/classes/UnknownHavingExpressionTypeError.md b/docs/reference/classes/UnknownHavingExpressionTypeError.md index 9b4e05f66b..6c5c848df1 100644 --- a/docs/reference/classes/UnknownHavingExpressionTypeError.md +++ b/docs/reference/classes/UnknownHavingExpressionTypeError.md @@ -3,7 +3,7 @@ id: UnknownHavingExpressionTypeError title: UnknownHavingExpressionTypeError --- -Defined in: [packages/db/src/errors.ts:1007](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1007) +Defined in: [packages/db/src/errors.ts:997](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L997) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1007](https://github.com/TanStack/db/blob new UnknownHavingExpressionTypeError(type): UnknownHavingExpressionTypeError; ``` -Defined in: [packages/db/src/errors.ts:1008](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1008) +Defined in: [packages/db/src/errors.ts:998](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L998) #### Parameters diff --git a/docs/reference/classes/UnsafeAliasPathError.md b/docs/reference/classes/UnsafeAliasPathError.md index 51f590e15f..ea9f4fe62f 100644 --- a/docs/reference/classes/UnsafeAliasPathError.md +++ b/docs/reference/classes/UnsafeAliasPathError.md @@ -3,7 +3,7 @@ id: UnsafeAliasPathError title: UnsafeAliasPathError --- -Defined in: [packages/db/src/errors.ts:704](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L704) +Defined in: [packages/db/src/errors.ts:694](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L694) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:704](https://github.com/TanStack/db/blob/ new UnsafeAliasPathError(segment): UnsafeAliasPathError; ``` -Defined in: [packages/db/src/errors.ts:705](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L705) +Defined in: [packages/db/src/errors.ts:695](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L695) #### Parameters diff --git a/docs/reference/classes/UnsupportedAggregateFunctionError.md b/docs/reference/classes/UnsupportedAggregateFunctionError.md index d5b1dfdf0f..0c7b4f2833 100644 --- a/docs/reference/classes/UnsupportedAggregateFunctionError.md +++ b/docs/reference/classes/UnsupportedAggregateFunctionError.md @@ -3,7 +3,7 @@ id: UnsupportedAggregateFunctionError title: UnsupportedAggregateFunctionError --- -Defined in: [packages/db/src/errors.ts:987](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L987) +Defined in: [packages/db/src/errors.ts:977](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L977) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:987](https://github.com/TanStack/db/blob/ new UnsupportedAggregateFunctionError(functionName): UnsupportedAggregateFunctionError; ``` -Defined in: [packages/db/src/errors.ts:988](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L988) +Defined in: [packages/db/src/errors.ts:978](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L978) #### Parameters diff --git a/docs/reference/classes/UnsupportedFnSelectResultError.md b/docs/reference/classes/UnsupportedFnSelectResultError.md index 341266aed3..0ee2f78edd 100644 --- a/docs/reference/classes/UnsupportedFnSelectResultError.md +++ b/docs/reference/classes/UnsupportedFnSelectResultError.md @@ -3,7 +3,7 @@ id: UnsupportedFnSelectResultError title: UnsupportedFnSelectResultError --- -Defined in: [packages/db/src/errors.ts:749](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L749) +Defined in: [packages/db/src/errors.ts:739](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L739) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:749](https://github.com/TanStack/db/blob/ new UnsupportedFnSelectResultError(valueDescription): UnsupportedFnSelectResultError; ``` -Defined in: [packages/db/src/errors.ts:750](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L750) +Defined in: [packages/db/src/errors.ts:740](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L740) #### Parameters diff --git a/docs/reference/classes/UnsupportedFromTypeError.md b/docs/reference/classes/UnsupportedFromTypeError.md index 0df5173fd1..d97777aa20 100644 --- a/docs/reference/classes/UnsupportedFromTypeError.md +++ b/docs/reference/classes/UnsupportedFromTypeError.md @@ -3,7 +3,7 @@ id: UnsupportedFromTypeError title: UnsupportedFromTypeError --- -Defined in: [packages/db/src/errors.ts:841](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L841) +Defined in: [packages/db/src/errors.ts:831](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L831) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:841](https://github.com/TanStack/db/blob/ new UnsupportedFromTypeError(type): UnsupportedFromTypeError; ``` -Defined in: [packages/db/src/errors.ts:842](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L842) +Defined in: [packages/db/src/errors.ts:832](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L832) #### Parameters diff --git a/docs/reference/classes/UnsupportedJoinSourceTypeError.md b/docs/reference/classes/UnsupportedJoinSourceTypeError.md index ce7248b61f..b3604c731c 100644 --- a/docs/reference/classes/UnsupportedJoinSourceTypeError.md +++ b/docs/reference/classes/UnsupportedJoinSourceTypeError.md @@ -3,7 +3,7 @@ id: UnsupportedJoinSourceTypeError title: UnsupportedJoinSourceTypeError --- -Defined in: [packages/db/src/errors.ts:959](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L959) +Defined in: [packages/db/src/errors.ts:949](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L949) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:959](https://github.com/TanStack/db/blob/ new UnsupportedJoinSourceTypeError(type): UnsupportedJoinSourceTypeError; ``` -Defined in: [packages/db/src/errors.ts:960](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L960) +Defined in: [packages/db/src/errors.ts:950](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L950) #### Parameters diff --git a/docs/reference/classes/UnsupportedJoinTypeError.md b/docs/reference/classes/UnsupportedJoinTypeError.md index 5215a14c85..ddb65bdc50 100644 --- a/docs/reference/classes/UnsupportedJoinTypeError.md +++ b/docs/reference/classes/UnsupportedJoinTypeError.md @@ -3,7 +3,7 @@ id: UnsupportedJoinTypeError title: UnsupportedJoinTypeError --- -Defined in: [packages/db/src/errors.ts:899](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L899) +Defined in: [packages/db/src/errors.ts:889](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L889) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:899](https://github.com/TanStack/db/blob/ new UnsupportedJoinTypeError(joinType): UnsupportedJoinTypeError; ``` -Defined in: [packages/db/src/errors.ts:900](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L900) +Defined in: [packages/db/src/errors.ts:890](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L890) #### Parameters diff --git a/docs/reference/classes/UnsupportedRootScalarSelectError.md b/docs/reference/classes/UnsupportedRootScalarSelectError.md index d1f378beab..cc8e714fc7 100644 --- a/docs/reference/classes/UnsupportedRootScalarSelectError.md +++ b/docs/reference/classes/UnsupportedRootScalarSelectError.md @@ -3,7 +3,7 @@ id: UnsupportedRootScalarSelectError title: UnsupportedRootScalarSelectError --- -Defined in: [packages/db/src/errors.ts:761](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L761) +Defined in: [packages/db/src/errors.ts:751](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L751) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:761](https://github.com/TanStack/db/blob/ new UnsupportedRootScalarSelectError(): UnsupportedRootScalarSelectError; ``` -Defined in: [packages/db/src/errors.ts:762](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L762) +Defined in: [packages/db/src/errors.ts:752](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L752) #### Returns diff --git a/docs/reference/functions/compileExpression.md b/docs/reference/functions/compileExpression.md index fb7e934830..86f62d4310 100644 --- a/docs/reference/functions/compileExpression.md +++ b/docs/reference/functions/compileExpression.md @@ -4,7 +4,7 @@ title: compileExpression --- ```ts -function compileExpression(expr, isSingleRow): CompiledSingleRowExpression | CompiledExpression; +function compileExpression(expr, isSingleRow): CompiledExpression | CompiledSingleRowExpression; ``` Defined in: [packages/db/src/query/compiler/evaluators.ts:103](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/evaluators.ts#L103) @@ -24,4 +24,4 @@ This eliminates branching during evaluation by pre-compiling the expression stru ## Returns -`CompiledSingleRowExpression` \| `CompiledExpression` +`CompiledExpression` \| `CompiledSingleRowExpression` diff --git a/docs/reference/functions/joinOf.md b/docs/reference/functions/joinOf.md deleted file mode 100644 index 69f922caf9..0000000000 --- a/docs/reference/functions/joinOf.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -id: joinOf -title: joinOf ---- - -```ts -function joinOf( - type, - source, -on): JoinOperator; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L78) - -A join whose type is chosen at run time. - -## Type Parameters - -### TJoinType - -`TJoinType` *extends* [`JoinType`](../type-aliases/JoinType.md) - -### TSource - -`TSource` *extends* [`Source`](../type-aliases/Source.md) - -### TContext - -`TContext` *extends* [`Context`](../interfaces/Context.md) = `any` - -## Parameters - -### type - -`TJoinType` - -### source - -`TSource` - -### on - -[`OperatorCallback`](../type-aliases/OperatorCallback.md)\<`TContext`, [`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends CollectionOptionsIdentity\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\>\> - -## Returns - -[`JoinOperator`](../interfaces/JoinOperator.md)\<`TContext`, `TSource`, `TJoinType`\> diff --git a/docs/reference/index.md b/docs/reference/index.md index 28959dd00c..3d5ed54abf 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -60,7 +60,6 @@ title: "@tanstack/db" - [JoinCollectionNotFoundError](classes/JoinCollectionNotFoundError.md) - [JoinConditionMustBeEqualityError](classes/JoinConditionMustBeEqualityError.md) - [JoinError](classes/JoinError.md) -- [JoinOperatorRequiredError](classes/JoinOperatorRequiredError.md) - [KeyUpdateNotAllowedError](classes/KeyUpdateNotAllowedError.md) - [LimitOffsetRequireOrderByError](classes/LimitOffsetRequireOrderByError.md) - [LiveQueryObserverDisposedError](classes/LiveQueryObserverDisposedError.md) @@ -160,7 +159,6 @@ title: "@tanstack/db" - [IndexOptions](interfaces/IndexOptions.md) - [IndexSuggestion](interfaces/IndexSuggestion.md) - [InsertConfig](interfaces/InsertConfig.md) -- [JoinOperator](interfaces/JoinOperator.md) - [LiveQueryCollectionConfig](interfaces/LiveQueryCollectionConfig.md) - [LiveQueryObserver](interfaces/LiveQueryObserver.md) - [LiveQuerySnapshot](interfaces/LiveQuerySnapshot.md) @@ -255,9 +253,7 @@ title: "@tanstack/db" - [InputRow](type-aliases/InputRow.md) - [InsertMutationFn](type-aliases/InsertMutationFn.md) - [InsertMutationFnParams](type-aliases/InsertMutationFnParams.md) -- [JoinCondition](type-aliases/JoinCondition.md) - [JoinOnCallback](type-aliases/JoinOnCallback.md) -- [JoinType](type-aliases/JoinType.md) - [KeyedNamespacedRow](type-aliases/KeyedNamespacedRow.md) - [KeyedStream](type-aliases/KeyedStream.md) - [LiveQueryCollectionUtils](type-aliases/LiveQueryCollectionUtils.md) @@ -337,14 +333,10 @@ title: "@tanstack/db" ## Variables - [collectionOptionsClaim](variables/collectionOptionsClaim.md) -- [fullJoin](variables/fullJoin.md) -- [innerJoin](variables/innerJoin.md) -- [leftJoin](variables/leftJoin.md) - [operators](variables/operators.md) - [PERSISTED\_READINESS](variables/PERSISTED_READINESS.md) - [pooledLiveQueries](variables/pooledLiveQueries.md) - [Query](variables/Query.md) -- [rightJoin](variables/rightJoin.md) - [SYNC\_PERSISTENCE\_PROTOCOL](variables/SYNC_PERSISTENCE_PROTOCOL.md) - [SYNC\_PERSISTENCE\_VERSION](variables/SYNC_PERSISTENCE_VERSION.md) @@ -428,7 +420,6 @@ title: "@tanstack/db" - [isNull](functions/isNull.md) - [isSingleResultCollection](functions/isSingleResultCollection.md) - [isUndefined](functions/isUndefined.md) -- [joinOf](functions/joinOf.md) - [length](functions/length.md) - [like](functions/like.md) - [liveQueryCollectionOptions](functions/liveQueryCollectionOptions.md) diff --git a/docs/reference/interfaces/JoinOperator.md b/docs/reference/interfaces/JoinOperator.md deleted file mode 100644 index b9a9fc0596..0000000000 --- a/docs/reference/interfaces/JoinOperator.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -id: JoinOperator -title: JoinOperator ---- - -Defined in: [packages/db/src/query/builder/join-operators.ts:18](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L18) - -A join step for `query.join(...)`. Importing a join operator is what brings -the join compiler into a bundle. - -## Type Parameters - -### TContext - -`TContext` *extends* [`Context`](Context.md) - -### TSource - -`TSource` *extends* [`Source`](../type-aliases/Source.md) - -### TJoinType - -`TJoinType` *extends* [`JoinType`](../type-aliases/JoinType.md) - -## Properties - -### on - -```ts -on: JoinOnCallback ? InferCollectionType : TSource[K] extends CollectionOptionsIdentity ? InferCollectionType : TSource[K] extends QueryBuilder ? ResultValue : never }[K] }>>; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L24) - -*** - -### runtime - -```ts -runtime: object; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L28) - -#### compile() - -```ts -compile: (pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, localFrom, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, valueIdentity, parentKeyStream?) => NamespacedAndKeyedStream = processJoins; -``` - -Processes all join clauses, applying lazy loading optimizations and maintaining -alias tracking for per-alias subscriptions (enables self-joins). - -##### Parameters - -###### pipeline - -[`NamespacedAndKeyedStream`](../type-aliases/NamespacedAndKeyedStream.md) - -###### joinClauses - -[`JoinClause`](../@tanstack/namespaces/IR/interfaces/JoinClause.md)[] - -###### sources - -`Record`\<`string`, [`KeyedStream`](../type-aliases/KeyedStream.md)\> - -###### mainCollectionId - -`string` - -###### mainSource - -`string` - -###### allInputs - -`Record`\<`string`, [`KeyedStream`](../type-aliases/KeyedStream.md)\> - -###### cache - -`QueryCache` - -###### queryMapping - -`QueryMapping` - -###### collections - -`Record`\<`string`, [`Collection`](Collection.md)\> - -###### subscriptions - -`Record`\<`string`, `CollectionSubscription`\> - -###### callbacks - -`Record`\<`string`, `LazyCollectionCallbacks`\> - -###### lazySources - -`Set`\<`string`\> - -###### optimizableOrderByCollections - -`Record`\<`string`, `OrderByOptimizationInfo`\> - -###### setWindowFn - -(`windowFn`) => `void` - -###### rawQuery - -[`QueryIR`](../@tanstack/namespaces/IR/interfaces/QueryIR.md) - -###### localFrom - -[`From`](../@tanstack/namespaces/IR/type-aliases/From.md) - -###### onCompileSubquery - -(...`args`) => `CompilationResult` - -###### aliasToCollectionId - -`Record`\<`string`, `string`\> - -###### aliasRemapping - -`Record`\<`string`, `string`\> - -###### sourceWhereClauses - -`Map`\<`string`, [`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md)\<`boolean`\>\> - -###### mainSourceIsParentFiltered - -`boolean` - -###### valueIdentity - -`ValueIdentity` - -###### parentKeyStream? - -[`KeyedStream`](../type-aliases/KeyedStream.md) - -##### Returns - -[`NamespacedAndKeyedStream`](../type-aliases/NamespacedAndKeyedStream.md) - -#### optimize() - -```ts -optimize: (query, groupedClauses) => QueryIR = applyOptimizations; -``` - -Step 4: Apply optimizations by lifting single-source clauses into subqueries. - -Creates a new QueryIR with single-source WHERE clauses moved to subqueries -that wrap the original table references. This ensures immutability and prevents -infinite recursion issues. - -##### Parameters - -###### query - -[`QueryIR`](../@tanstack/namespaces/IR/interfaces/QueryIR.md) - -Original QueryIR to optimize - -###### groupedClauses - -`GroupedWhereClauses` - -WHERE clauses grouped by optimization strategy - -##### Returns - -[`QueryIR`](../@tanstack/namespaces/IR/interfaces/QueryIR.md) - -New QueryIR with optimizations applied - -#### reducePublicKeys() - -```ts -reducePublicKeys: (compilation, getKey) => object; -``` - -Reduces a query without includes to one canonical row per public key. - -##### Parameters - -###### compilation - -`CompilationResult` - -###### getKey - -(`row`) => `unknown` - -##### Returns - -`object` - -###### facades - -```ts -facades: undefined; -``` - -###### pipeline - -```ts -pipeline: ResultStream; -``` - -#### validate() - -```ts -validate: (expression) => readonly [BasicExpression, BasicExpression][] = validateJoinConditions; -``` - -Validate join syntax and return its equality operands in source order. - -##### Parameters - -###### expression - -[`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md) - -##### Returns - -readonly \[[`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md), [`BasicExpression`](../@tanstack/namespaces/IR/type-aliases/BasicExpression.md)\][] - -*** - -### source - -```ts -source: TSource; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:23](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L23) - -*** - -### type - -```ts -type: TJoinType; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:27](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L27) diff --git a/docs/reference/type-aliases/ExtractContext.md b/docs/reference/type-aliases/ExtractContext.md index 7c5a4ec5a9..7e7cfbf233 100644 --- a/docs/reference/type-aliases/ExtractContext.md +++ b/docs/reference/type-aliases/ExtractContext.md @@ -7,7 +7,7 @@ title: ExtractContext type ExtractContext = T extends BaseQueryBuilder ? TContext : T extends QueryBuilder ? TContext : never; ``` -Defined in: [packages/db/src/query/builder/index.ts:1791](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1791) +Defined in: [packages/db/src/query/builder/index.ts:1882](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1882) ## Type Parameters diff --git a/docs/reference/type-aliases/InitialQueryBuilder.md b/docs/reference/type-aliases/InitialQueryBuilder.md index 46fa9d5627..6610c07989 100644 --- a/docs/reference/type-aliases/InitialQueryBuilder.md +++ b/docs/reference/type-aliases/InitialQueryBuilder.md @@ -7,4 +7,4 @@ title: InitialQueryBuilder type InitialQueryBuilder = Pick, "from" | "unionAll">; ``` -Defined in: [packages/db/src/query/builder/index.ts:1774](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1774) +Defined in: [packages/db/src/query/builder/index.ts:1865](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1865) diff --git a/docs/reference/type-aliases/JoinCondition.md b/docs/reference/type-aliases/JoinCondition.md deleted file mode 100644 index 2e1cd4d3d6..0000000000 --- a/docs/reference/type-aliases/JoinCondition.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -id: JoinCondition -title: JoinCondition ---- - -```ts -type JoinCondition = OperatorCallback>>>; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:46](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L46) - -A join condition. `.join()` supplies the query's context. A reusable -operator built outside `.join()` passes it explicitly: -`leftJoin<{ post: typeof posts }, ContextFromSource<{ user: typeof users }>>(...)`. -Without it, the condition is untyped. - -## Type Parameters - -### TContext - -`TContext` *extends* [`Context`](../interfaces/Context.md) - -### TSource - -`TSource` *extends* [`Source`](Source.md) diff --git a/docs/reference/type-aliases/JoinType.md b/docs/reference/type-aliases/JoinType.md deleted file mode 100644 index 9e848bd012..0000000000 --- a/docs/reference/type-aliases/JoinType.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -id: JoinType -title: JoinType ---- - -```ts -type JoinType = "inner" | "left" | "right" | "full"; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:12](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L12) diff --git a/docs/reference/type-aliases/LiveQueryCollectionUtils.md b/docs/reference/type-aliases/LiveQueryCollectionUtils.md index 97d800ad0c..96883c3423 100644 --- a/docs/reference/type-aliases/LiveQueryCollectionUtils.md +++ b/docs/reference/type-aliases/LiveQueryCollectionUtils.md @@ -7,7 +7,7 @@ title: LiveQueryCollectionUtils type LiveQueryCollectionUtils = UtilsRecord & object; ``` -Defined in: [packages/db/src/query/live/collection-config-builder.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/collection-config-builder.ts#L59) +Defined in: [packages/db/src/query/live/collection-config-builder.ts:60](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/collection-config-builder.ts#L60) ## Type Declaration diff --git a/docs/reference/type-aliases/MergeContextForJoinCallback.md b/docs/reference/type-aliases/MergeContextForJoinCallback.md index e1177322c8..6c66a8863e 100644 --- a/docs/reference/type-aliases/MergeContextForJoinCallback.md +++ b/docs/reference/type-aliases/MergeContextForJoinCallback.md @@ -22,7 +22,7 @@ Optionality is only applied to the result AFTER the join logic executes. **Example**: ```typescript .from({ users }) -.join(leftJoin({ orders }, ({ users, orders }) => { +.leftJoin({ orders }, ({ users, orders }) => { // users is NOT optional here - we can access users.id directly // orders is NOT optional here - we can access orders.userId directly return eq(users.id, orders.userId) diff --git a/docs/reference/type-aliases/QueryBuilder.md b/docs/reference/type-aliases/QueryBuilder.md index c0ff115b32..fbfefb5baa 100644 --- a/docs/reference/type-aliases/QueryBuilder.md +++ b/docs/reference/type-aliases/QueryBuilder.md @@ -7,7 +7,7 @@ title: QueryBuilder type QueryBuilder = Omit, "from" | "unionAll" | "_getQuery">; ``` -Defined in: [packages/db/src/query/builder/index.ts:1781](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1781) +Defined in: [packages/db/src/query/builder/index.ts:1872](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1872) ## Type Parameters diff --git a/docs/reference/type-aliases/QueryResult.md b/docs/reference/type-aliases/QueryResult.md index d71e624d2d..087d8d5d4d 100644 --- a/docs/reference/type-aliases/QueryResult.md +++ b/docs/reference/type-aliases/QueryResult.md @@ -7,7 +7,7 @@ title: QueryResult type QueryResult = GetResult>; ``` -Defined in: [packages/db/src/query/builder/index.ts:1799](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1799) +Defined in: [packages/db/src/query/builder/index.ts:1890](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1890) ## Type Parameters diff --git a/docs/reference/type-aliases/SourceClauseContext.md b/docs/reference/type-aliases/SourceClauseContext.md index 3636ce95ee..564128dc60 100644 --- a/docs/reference/type-aliases/SourceClauseContext.md +++ b/docs/reference/type-aliases/SourceClauseContext.md @@ -7,4 +7,4 @@ title: SourceClauseContext type SourceClauseContext = "from clause" | "unionAll clause" | "join clause"; ``` -Defined in: [packages/db/src/errors.ts:633](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L633) +Defined in: [packages/db/src/errors.ts:623](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L623) diff --git a/docs/reference/variables/Query.md b/docs/reference/variables/Query.md index 85d2435c1f..5048c18820 100644 --- a/docs/reference/variables/Query.md +++ b/docs/reference/variables/Query.md @@ -7,4 +7,4 @@ title: Query const Query: InitialQueryBuilderConstructor = BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:1788](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1788) +Defined in: [packages/db/src/query/builder/index.ts:1879](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1879) diff --git a/docs/reference/variables/fullJoin.md b/docs/reference/variables/fullJoin.md deleted file mode 100644 index 1372c735b1..0000000000 --- a/docs/reference/variables/fullJoin.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -id: fullJoin -title: fullJoin ---- - -```ts -const fullJoin: JoinOperatorFunction<"full">; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:75](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L75) diff --git a/docs/reference/variables/innerJoin.md b/docs/reference/variables/innerJoin.md deleted file mode 100644 index 09c5a8d13b..0000000000 --- a/docs/reference/variables/innerJoin.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -id: innerJoin -title: innerJoin ---- - -```ts -const innerJoin: JoinOperatorFunction<"inner">; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L74) diff --git a/docs/reference/variables/leftJoin.md b/docs/reference/variables/leftJoin.md deleted file mode 100644 index a8b3650be0..0000000000 --- a/docs/reference/variables/leftJoin.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -id: leftJoin -title: leftJoin ---- - -```ts -const leftJoin: JoinOperatorFunction<"left">; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L72) diff --git a/docs/reference/variables/rightJoin.md b/docs/reference/variables/rightJoin.md deleted file mode 100644 index 83f29faa38..0000000000 --- a/docs/reference/variables/rightJoin.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -id: rightJoin -title: rightJoin ---- - -```ts -const rightJoin: JoinOperatorFunction<"right">; -``` - -Defined in: [packages/db/src/query/builder/join-operators.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/join-operators.ts#L73) diff --git a/examples/react/projects/README.md b/examples/react/projects/README.md index cc5d09f9cf..17db72bfc6 100644 --- a/examples/react/projects/README.md +++ b/examples/react/projects/README.md @@ -78,7 +78,7 @@ Now that you have two routes you can use a `Link` component to navigate between To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. ```tsx -import { Link } from '@tanstack/react-router' +import { Link } from "@tanstack/react-router" ``` Then anywhere in your JSX you can use it like so: @@ -98,10 +98,10 @@ In the File Based Routing setup the layout is located in `src/routes/__root.tsx` Here is an example layout that includes a header: ```tsx -import { Outlet, createRootRoute } from '@tanstack/react-router' -import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' +import { Outlet, createRootRoute } from "@tanstack/react-router" +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" -import { Link } from '@tanstack/react-router' +import { Link } from "@tanstack/react-router" export const Route = createRootRoute({ component: () => ( @@ -132,9 +132,9 @@ For example: ```tsx const peopleRoute = createRoute({ getParentRoute: () => rootRoute, - path: '/people', + path: "/people", loader: async () => { - const response = await fetch('https://swapi.dev/api/people') + const response = await fetch("https://swapi.dev/api/people") return response.json() as Promise<{ results: { name: string @@ -180,16 +180,16 @@ Built on a TypeScript implementation of differential dataflow, TanStack DB provi This example uses Query Collections for server-state synchronization with tRPC: ```tsx -import { createCollection } from '@tanstack/react-db' -import { queryCollectionOptions } from '@tanstack/query-db-collection' -import { QueryClient } from '@tanstack/query-core' +import { createCollection } from "@tanstack/react-db" +import { queryCollectionOptions } from "@tanstack/query-db-collection" +import { QueryClient } from "@tanstack/query-core" const queryClient = new QueryClient() export const todoCollection = createCollection( queryCollectionOptions({ - id: 'todos', - queryKey: ['todos'], + id: "todos", + queryKey: ["todos"], queryFn: async () => { const todos = await trpc.todos.getAll.query() return todos.map((todo) => ({ @@ -228,7 +228,7 @@ const AddTodo = () => { onClick={() => todoCollection.insert({ id: crypto.randomUUID(), - text: '🔥 Make app faster', + text: "🔥 Make app faster", completed: false, }) } @@ -242,14 +242,14 @@ const AddTodo = () => { Use live queries to read data reactively across collections: ```tsx -import { eq, innerJoin, useLiveQuery } from '@tanstack/react-db' +import { eq, useLiveQuery } from "@tanstack/react-db" const Todos = () => { // Read data using live queries with cross-collection joins const { data: todos } = useLiveQuery((query) => query .from({ t: todoCollection }) - .join(innerJoin({ l: listCollection }, ({ t, l }) => eq(l.id, t.list_id))) + .innerJoin({ l: listCollection }, ({ t, l }) => eq(l.id, t.list_id)) .where(({ l }) => eq(l.active, true)) .select(({ t, l }) => ({ id: t.id, diff --git a/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts b/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts index f40b75ee61..4958bb894d 100644 --- a/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts +++ b/packages/db-collection-e2e/src/suites/joins-oracle.suite.ts @@ -4,13 +4,7 @@ * Tests multi-collection joins with various syncMode combinations */ import { randomUUID } from 'node:crypto' -import { - createLiveQueryCollection, - eq, - gt, - isNull, - leftJoin, -} from '@tanstack/db' +import { createLiveQueryCollection, eq, gt, isNull } from '@tanstack/db' import { describe, expect, it, vi } from 'vitest' import { waitFor, waitForQueryData } from '../utils/helpers' @@ -388,10 +382,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join( - leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ), + .join({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), ) .select(({ user, post }) => ({ id: post.id, @@ -443,10 +435,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { q .from({ user: usersCollection }) .where(({ user }) => eq(user.isActive, true)) - .join( - leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ), + .join({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), ) .where(({ post }) => gt(post.viewCount, 10)) .select(({ user, post }) => ({ @@ -496,10 +486,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersEager }) - .join( - leftJoin({ post: postsOnDemand }, ({ user, post }) => - eq(user.id, post.userId), - ), + .join({ post: postsOnDemand }, ({ user, post }) => + eq(user.id, post.userId), ) .select(({ user, post }) => ({ id: post.id, @@ -548,10 +536,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join( - leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ), + .join({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), ) .orderBy(({ post }) => post.viewCount, `desc`) .select(({ user, post }) => ({ @@ -620,10 +606,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join( - leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ), + .join({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), ) .orderBy(({ post }) => post.id, `asc`) .limit(10) @@ -670,15 +654,11 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join( - leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ), + .join({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), ) - .join( - leftJoin({ comment: comments }, ({ post, comment }) => - eq(post.id, comment.postId), - ), + .join({ comment: comments }, ({ post, comment }) => + eq(post.id, comment.postId), ) .select(({ user, post, comment }) => ({ id: comment.id, @@ -732,16 +712,12 @@ export function createJoinsTestSuite(getConfig: () => Promise) { q .from({ user: users }) .where(({ user }) => eq(user.isActive, true)) - .join( - leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ), + .join({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), ) .where(({ post }) => isNull(post.deletedAt)) - .join( - leftJoin({ comment: comments }, ({ post, comment }) => - eq(post.id, comment.postId), - ), + .join({ comment: comments }, ({ post, comment }) => + eq(post.id, comment.postId), ) .where(({ comment }) => isNull(comment.deletedAt)) .select(({ user, post, comment }) => ({ @@ -793,15 +769,11 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: usersEager }) - .join( - leftJoin({ post: postsOnDemand }, ({ user, post }) => - eq(user.id, post.userId), - ), + .join({ post: postsOnDemand }, ({ user, post }) => + eq(user.id, post.userId), ) - .join( - leftJoin({ comment: commentsOnDemand }, ({ post, comment }) => - eq(post.id, comment.postId), - ), + .join({ comment: commentsOnDemand }, ({ post, comment }) => + eq(post.id, comment.postId), ) .select(({ user, post, comment }) => ({ id: comment.id, @@ -850,10 +822,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join( - leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ), + .join({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), ) .where(({ post }) => gt(post.viewCount, 50)) .select(({ user, post }) => ({ @@ -901,10 +871,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { q .from({ user: users }) .where(({ user }) => gt(user.age, 30)) - .join( - leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ), + .join({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), ) .select(({ user, post }) => ({ id: post.id, @@ -951,10 +919,8 @@ export function createJoinsTestSuite(getConfig: () => Promise) { const query = createLiveQueryCollection((q) => q .from({ user: users }) - .join( - leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ), + .leftJoin({ post: posts }, ({ user, post }) => + eq(user.id, post.userId), ) .select(({ user, post }) => ({ id: user.id, diff --git a/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts b/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts index 255f205fb6..6753248b99 100644 --- a/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts +++ b/packages/db-sqlite-persistence-core/tests/persisted-readiness-oracle.test.ts @@ -6,7 +6,6 @@ import { createLiveQueryObserver, eq, getPersistedReadinessSource, - leftJoin, waitForPersistedInitialRender, } from '@tanstack/db' import { describe, expect, it, vi } from 'vitest' @@ -264,11 +263,7 @@ describe(`persisted-readiness oracle`, () => { query: (q) => q .from({ left }) - .join( - leftJoin({ right }, ({ left: a, right: b }) => - eq(a.id, b.id), - ), - ), + .join({ right }, ({ left: a, right: b }) => eq(a.id, b.id)), }) const observer = createLiveQueryObserver(query) await checkWithCleanup(() => { @@ -347,7 +342,7 @@ describe(`persisted-readiness oracle`, () => { query: (q) => q .from({ a: sourceA }) - .join(leftJoin({ b: sourceB }, ({ a, b }) => eq(a.id, b.id))), + .join({ b: sourceB }, ({ a, b }) => eq(a.id, b.id)), }) const observer = createLiveQueryObserver(query) const unsubscribe = observer.subscribe(() => {}) @@ -408,7 +403,7 @@ describe(`persisted-readiness oracle`, () => { query: (q) => q .from({ a: sourceA }) - .join(leftJoin({ b: sourceB }, ({ a, b }) => eq(a.id, b.id))), + .join({ b: sourceB }, ({ a, b }) => eq(a.id, b.id)), }) const observer = createLiveQueryObserver(query) const unsubscribe = observer.subscribe(() => {}) @@ -671,7 +666,7 @@ describe(`persisted-readiness oracle`, () => { query: (q) => q .from({ a: sourceA }) - .join(leftJoin({ b: sourceB }, ({ a, b }) => eq(a.id, b.id))), + .join({ b: sourceB }, ({ a, b }) => eq(a.id, b.id)), }) observer = createLiveQueryObserver(query) unsubscribe = observer.subscribe(() => {}) @@ -1150,7 +1145,7 @@ describe(`persisted-readiness oracle`, () => { query: (q) => q .from({ a: sourceA }) - .join(leftJoin({ b: sourceB }, ({ a, b }) => eq(a.id, b.id))), + .join({ b: sourceB }, ({ a, b }) => eq(a.id, b.id)), }) const observer = createLiveQueryObserver(query) const unsubscribe = observer.subscribe(() => {}) diff --git a/packages/db/mangle-cache.json b/packages/db/mangle-cache.json index f5e125474e..7069541802 100644 --- a/packages/db/mangle-cache.json +++ b/packages/db/mangle-cache.json @@ -416,6 +416,5 @@ "coverSource": "fN", "windowRequestGeneration": "av", "latestWindowRequest": "gE", - "includesRuntimeCache": "gT", - "findJoinClause": "gU" + "includesRuntimeCache": "gT" } diff --git a/packages/db/skills/db-core/live-queries/SKILL.md b/packages/db/skills/db-core/live-queries/SKILL.md index b4636a3e09..677f6f4c42 100644 --- a/packages/db/skills/db-core/live-queries/SKILL.md +++ b/packages/db/skills/db-core/live-queries/SKILL.md @@ -115,18 +115,16 @@ non-null value. ### 2. Joining two collections -Join conditions accept `eq()` or a nonempty, possibly nested `and()` of equalities (equality only -- IVM constraint). Joins take an imported operator: `.join(leftJoin(source, on))`, and likewise `innerJoin`, `rightJoin`, `fullJoin`, and `joinOf(type, source, on)`. The `.join(source, on)` call shape throws `JoinOperatorRequiredError`. The `.leftJoin()`, `.innerJoin()`, `.rightJoin()`, and `.fullJoin()` methods are removed. +Join conditions accept `eq()` or a nonempty, possibly nested `and()` of equalities (equality only -- IVM constraint). Default join type is `left`. Convenience methods: `leftJoin`, `rightJoin`, `innerJoin`, `fullJoin`. ```ts -import { createLiveQueryCollection, eq, innerJoin } from '@tanstack/db' +import { createLiveQueryCollection, eq } from '@tanstack/db' const userPosts = createLiveQueryCollection((q) => q .from({ user: usersCollection }) - .join( - innerJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ), + .innerJoin({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), ) .select(({ user, post }) => ({ userName: user.name, @@ -139,35 +137,15 @@ Multiple joins: ```ts q.from({ user: usersCollection }) - .join( - leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ), - ) - .join( - leftJoin({ comment: commentsCollection }, ({ post, comment }) => - eq(post.id, comment.postId), - ), + .join({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId)) + .join({ comment: commentsCollection }, ({ post, comment }) => + eq(post.id, comment.postId), ) ``` -A reusable operator built outside `.join()` needs the query's context as a type argument. Without it, the condition is untyped: - -```ts -import { eq, leftJoin } from '@tanstack/db' -import type { ContextFromSource } from '@tanstack/db' - -type UserQuery = ContextFromSource<{ user: typeof usersCollection }> -export const withPosts = leftJoin<{ post: typeof postsCollection }, UserQuery>( - { post: postsCollection }, - ({ user, post }) => eq(user.id, post.userId), -) -q.from({ user: usersCollection }).join(withPosts) -``` - ### 3. Aggregation with groupBy + having -Use `groupBy` to group rows, then aggregate in `select`. Filter groups with `having`. The `$selected` namespace lets `having` and `orderBy` reference fields defined in `select`. A `groupBy` with no aggregate wraps its keys in `groupKeys()`: `.groupBy(groupKeys(({ order }) => order.status))`; without it the query throws `GroupingOperatorRequiredError`. A `groupKeys()` built outside `.groupBy()` takes the context as a type argument, `groupKeys>(...)`, as a reusable join does; without it the callback is untyped. +Use `groupBy` to group rows, then aggregate in `select`. Filter groups with `having`. The `$selected` namespace lets `having` and `orderBy` reference fields defined in `select`. A `groupBy` with no aggregate wraps its keys in `groupKeys()`: `.groupBy(groupKeys(({ order }) => order.status))`; without it the query throws `GroupingOperatorRequiredError`. A `groupKeys()` built outside `.groupBy()` takes the context as a type argument, `groupKeys>(...)`; without it the callback is untyped. ```ts import { count, sum, gt } from '@tanstack/db' @@ -212,10 +190,8 @@ const activeUsers = createLiveQueryCollection((q) => const activeUserPosts = createLiveQueryCollection((q) => q .from({ user: activeUsers }) - .join( - leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ), + .join({ post: postsCollection }, ({ user, post }) => + eq(user.id, post.userId), ) .select(({ user, post }) => ({ userName: user.name, @@ -556,12 +532,12 @@ q.from({ order: ordersCollection }) ### Compound joins and acquisition ```ts -import { and, eq, leftJoin } from '@tanstack/db' +import { and, eq } from '@tanstack/db' -q.from({ product: productsCollection }).join( - leftJoin({ stock: stockCollection }, ({ product, stock }) => +q.from({ product: productsCollection }).leftJoin( + { stock: stockCollection }, + ({ product, stock }) => and(eq(product.sku, stock.sku), eq(product.region, stock.region)), - ), ) ``` @@ -595,16 +571,14 @@ The differential dataflow join operator supports a single equality or nested `an ```ts // WRONG q.from({ user: usersCollection }).join( - leftJoin({ post: postsCollection }, ({ user, post }) => - gt(user.id, post.userId), - ), + { post: postsCollection }, + ({ user, post }) => gt(user.id, post.userId), ) // CORRECT q.from({ user: usersCollection }).join( - leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ), + { post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId), ) ``` diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index aebd727674..4b4584e89e 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -620,16 +620,6 @@ export class IncludeOperatorRequiredError extends QueryBuilderError { } } -export class JoinOperatorRequiredError extends QueryBuilderError { - constructor(type: string) { - super( - devBuild() && process.env.NODE_ENV !== `production` - ? `.join() takes a join operator, such as .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Import leftJoin, innerJoin, rightJoin, fullJoin, or joinOf from @tanstack/db. Got: ${type}` - : codedMessage(245, { type }), - ) - } -} - export type SourceClauseContext = `from clause` | `unionAll clause` | `join clause` @@ -646,7 +636,7 @@ export class InvalidSourceTypeError extends QueryBuilderError { context === `unionAll clause` ? `.unionAll({ todos: todosCollection, events: eventsCollection })` : context === `join clause` - ? `.join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id)))` + ? `.join({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))` : `.from({ todos: todosCollection })` return ( `Invalid source for ${context}: Expected ${expected}. ` + diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index 11a3c4ef71..c2b63b8243 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -22,11 +22,11 @@ import { InvalidSourceError, InvalidSourceTypeError, InvalidWhereExpressionError, - JoinOperatorRequiredError, OnlyOneSourceAllowedError, QueryMustHaveFromClauseError, SubQueryMustHaveFromClauseError, } from '../../errors.js' +import { validateJoinConditions } from '../join-conditions.js' import { codedMessage, devBuild } from '../../error-message.js' import { getQueryIR } from './query-ir.js' import { cloneQueryForPlacement } from './clone-query.js' @@ -44,7 +44,6 @@ import { ToArrayWrapper, } from './functions.js' import type { CollectionResolver } from './clone-query.js' -import type { JoinOperator, JoinType } from './join-operators.js' import type { GroupKeys } from './group-keys.js' import type { IncludesRuntime } from '../live/includes-runtime.js' import type { SourceClauseContext } from '../../errors.js' @@ -69,6 +68,7 @@ import type { FunctionalHavingRow, GetResult, GroupByCallback, + JoinOnCallback, MergeContextForJoinCallback, MergeContextWithJoinType, NonScalarSelectObject, @@ -315,11 +315,9 @@ export class BaseQueryBuilder { /** * Join another table or subquery to the current query * - * @param operator - A join operator: `leftJoin`, `innerJoin`, `rightJoin`, - * `fullJoin`, or `joinOf(type, ...)`. Each takes an object with a single - * key-value pair (the alias and a Collection or subquery) and a callback - * that returns the join condition. Pass it inline so the callback is typed - * from this query. + * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery + * @param onCallback - A function that receives table references and returns the join condition + * @param type - The type of join: 'inner', 'left', 'right', or 'full' (defaults to 'left') * @returns A QueryBuilder with the joined table available * * @example @@ -327,39 +325,33 @@ export class BaseQueryBuilder { * // Left join users with posts * query * .from({ users: usersCollection }) - * .join(leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))) + * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) * - * // Inner join + * // Inner join with explicit type * query * .from({ u: usersCollection }) - * .join(innerJoin({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId))) + * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId), 'inner') + * ``` * - * // Join type chosen at run time + * // Join with a subquery + * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active) * query - * .from({ u: usersCollection }) - * .join(joinOf(type, { p: postsCollection }, ({u, p}) => eq(u.id, p.userId))) - * ``` + * .from({ activeUsers }) + * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId)) */ - join( - operator: JoinOperator, + join< + TSource extends Source, + TJoinType extends `inner` | `left` | `right` | `full` = `left`, + >( + source: TSource, + onCallback: JoinOnCallback< + MergeContextForJoinCallback> + >, + type: TJoinType = `left` as TJoinType, ): QueryBuilder< MergeContextWithJoinType, TJoinType> > { - // Untyped callers can still pass the removed (source, on, type) shape. - const value: unknown = operator - if ( - typeof value !== `object` || - value === null || - typeof (value as { runtime?: { compile?: unknown } }).runtime?.compile !== - `function` - ) - throw new JoinOperatorRequiredError( - value === null ? `null` : typeof value, - ) - const [alias, from] = this._createRefForSource( - operator.source, - `join clause`, - ) + const [alias, from] = this._createRefForSource(source, `join clause`) // Create a temporary context for the callback const currentAliases = this._getCurrentAliases() @@ -372,15 +364,10 @@ export class BaseQueryBuilder { > // Get the join condition expression - const onExpression = operator.on(refProxy) + const onExpression = onCallback(refProxy) - operator.runtime.validate(onExpression) - const joinClause: JoinClause = { - from, - type: operator.type, - on: onExpression, - runtime: operator.runtime, - } + validateJoinConditions(onExpression) + const joinClause: JoinClause = { from, type, on: onExpression } const existingJoins = this.query.join || [] @@ -390,6 +377,110 @@ export class BaseQueryBuilder { }) as any } + /** + * Perform a LEFT JOIN with another table or subquery + * + * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery + * @param onCallback - A function that receives table references and returns the join condition + * @returns A QueryBuilder with the left joined table available + * + * @example + * ```ts + * // Left join users with posts + * query + * .from({ users: usersCollection }) + * .leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) + * ``` + */ + leftJoin( + source: TSource, + onCallback: JoinOnCallback< + MergeContextForJoinCallback> + >, + ): QueryBuilder< + MergeContextWithJoinType, `left`> + > { + return this.join(source, onCallback, `left`) + } + + /** + * Perform a RIGHT JOIN with another table or subquery + * + * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery + * @param onCallback - A function that receives table references and returns the join condition + * @returns A QueryBuilder with the right joined table available + * + * @example + * ```ts + * // Right join users with posts + * query + * .from({ users: usersCollection }) + * .rightJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) + * ``` + */ + rightJoin( + source: TSource, + onCallback: JoinOnCallback< + MergeContextForJoinCallback> + >, + ): QueryBuilder< + MergeContextWithJoinType, `right`> + > { + return this.join(source, onCallback, `right`) + } + + /** + * Perform an INNER JOIN with another table or subquery + * + * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery + * @param onCallback - A function that receives table references and returns the join condition + * @returns A QueryBuilder with the inner joined table available + * + * @example + * ```ts + * // Inner join users with posts + * query + * .from({ users: usersCollection }) + * .innerJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) + * ``` + */ + innerJoin( + source: TSource, + onCallback: JoinOnCallback< + MergeContextForJoinCallback> + >, + ): QueryBuilder< + MergeContextWithJoinType, `inner`> + > { + return this.join(source, onCallback, `inner`) + } + + /** + * Perform a FULL JOIN with another table or subquery + * + * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery + * @param onCallback - A function that receives table references and returns the join condition + * @returns A QueryBuilder with the full joined table available + * + * @example + * ```ts + * // Full join users with posts + * query + * .from({ users: usersCollection }) + * .fullJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId)) + * ``` + */ + fullJoin( + source: TSource, + onCallback: JoinOnCallback< + MergeContextForJoinCallback> + >, + ): QueryBuilder< + MergeContextWithJoinType, `full`> + > { + return this.join(source, onCallback, `full`) + } + /** * Filter rows based on a condition * diff --git a/packages/db/src/query/builder/join-operators.ts b/packages/db/src/query/builder/join-operators.ts deleted file mode 100644 index 91c7acb914..0000000000 --- a/packages/db/src/query/builder/join-operators.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { joinRuntime } from '../compiler/join-runtime.js' -import type { JoinRuntime } from '../compiler/join-runtime.js' -import type { - Context, - JoinOnCallback, - MergeContextForJoinCallback, - OperatorCallback, - SchemaFromSource, - Source, -} from './types.js' - -export type JoinType = `inner` | `left` | `right` | `full` - -/** - * A join step for `query.join(...)`. Importing a join operator is what brings - * the join compiler into a bundle. - */ -export interface JoinOperator< - TContext extends Context, - TSource extends Source, - TJoinType extends JoinType, -> { - source: TSource - on: JoinOnCallback< - MergeContextForJoinCallback> - > - type: TJoinType - runtime: JoinRuntime -} - -/** A function that builds one join type's operator. */ -export type JoinOperatorFunction = < - TSource extends Source, - TContext extends Context = any, ->( - source: TSource, - on: JoinCondition, -) => JoinOperator - -/** - * A join condition. `.join()` supplies the query's context. A reusable - * operator built outside `.join()` passes it explicitly: - * `leftJoin<{ post: typeof posts }, ContextFromSource<{ user: typeof users }>>(...)`. - * Without it, the condition is untyped. - */ -export type JoinCondition< - TContext extends Context, - TSource extends Source, -> = OperatorCallback< - TContext, - JoinOnCallback< - MergeContextForJoinCallback> - > -> - -function joinOperator( - type: TJoinType, -): JoinOperatorFunction { - return ( - source: TSource, - on: JoinOnCallback< - MergeContextForJoinCallback> - >, - ): JoinOperator => ({ - source, - on, - type, - runtime: joinRuntime, - }) -} - -export const leftJoin: JoinOperatorFunction<`left`> = joinOperator(`left`) -export const rightJoin: JoinOperatorFunction<`right`> = joinOperator(`right`) -export const innerJoin: JoinOperatorFunction<`inner`> = joinOperator(`inner`) -export const fullJoin: JoinOperatorFunction<`full`> = joinOperator(`full`) - -/** A join whose type is chosen at run time. */ -export function joinOf< - TJoinType extends JoinType, - TSource extends Source, - TContext extends Context = any, ->( - type: TJoinType, - source: TSource, - on: JoinCondition, -): JoinOperator { - return joinOperator(type)(source, on) -} diff --git a/packages/db/src/query/builder/types.ts b/packages/db/src/query/builder/types.ts index cc04f4f1d7..259317cd03 100644 --- a/packages/db/src/query/builder/types.ts +++ b/packages/db/src/query/builder/types.ts @@ -1188,7 +1188,7 @@ type ResultValue = TContext[`hasResult`] extends true * from({ users }).where(...) // → User[] * * // Join query without select: - * from({ users }).join(leftJoin({ orders }, ...)) // → { users: User, orders: Order | undefined }[] + * from({ users }).leftJoin({ orders }, ...) // → { users: User, orders: Order | undefined }[] * * // Query with select: * from({ users }).select({ id: users.id, name: users.name }) // → { id: number, name: string }[] @@ -1350,7 +1350,7 @@ export type HasJoinType< * **Example**: * ```typescript * .from({ users }) - * .join(leftJoin({ orders }, ({ users, orders }) => { + * .leftJoin({ orders }, ({ users, orders }) => { * // users is NOT optional here - we can access users.id directly * // orders is NOT optional here - we can access orders.userId directly * return eq(users.id, orders.userId) diff --git a/packages/db/src/query/compiler/index.ts b/packages/db/src/query/compiler/index.ts index df3d2f2da5..2b550402d1 100644 --- a/packages/db/src/query/compiler/index.ts +++ b/packages/db/src/query/compiler/index.ts @@ -67,7 +67,7 @@ import { isCaseWhenConditionTrue, toBooleanPredicate, } from './evaluators.js' -import { registerLazyDemandPlan } from './lazy-demand.js' +import { processJoins, registerLazyDemandPlan } from './joins.js' import { findAggregate } from './find-aggregate.js' import { getLazyLoadTargets } from './lazy-targets.js' import { processOrderBy } from './order-by.js' @@ -101,7 +101,7 @@ import type { QueryRef, UnionAll, } from '../ir.js' -import type { LazyCollectionCallbacks } from './lazy-demand.js' +import type { LazyCollectionCallbacks } from './joins.js' import type { IStreamBuilder, KeyValue, MultiSet } from '@tanstack/db-ivm' import type { Collection } from '../../collection/index.js' import type { @@ -596,7 +596,7 @@ function compileQueryInternal( // Process JOIN clauses if they exist if (query.join && query.join.length > 0) { - pipeline = query.join[0]!.runtime.compile( + pipeline = processJoins( pipeline, query.join, sources, diff --git a/packages/db/src/query/compiler/join-runtime.ts b/packages/db/src/query/compiler/join-runtime.ts deleted file mode 100644 index 7324acb012..0000000000 --- a/packages/db/src/query/compiler/join-runtime.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { reducePublicKeys } from '../live/public-key-reduction.js' -import { validateJoinConditions } from '../join-conditions.js' -import { applyOptimizations } from '../optimizer-joins.js' -import { processJoins } from './joins.js' - -/** - * The join compiler and its helpers. Join operators carry it onto each IR join - * clause, so apps without joins do not bundle it. - */ -export const joinRuntime = { - compile: processJoins, - optimize: applyOptimizations, - // A joined query with a custom key reduces its public keys. - reducePublicKeys, - validate: validateJoinConditions, -} - -export type JoinRuntime = typeof joinRuntime diff --git a/packages/db/src/query/compiler/joins.ts b/packages/db/src/query/compiler/joins.ts index 8456762246..65f1e38092 100644 --- a/packages/db/src/query/compiler/joins.ts +++ b/packages/db/src/query/compiler/joins.ts @@ -29,7 +29,6 @@ import { } from '../ir.js' import { compileExpression } from './evaluators.js' import { getLazyLoadTargets } from './lazy-targets.js' -import { registerLazyDemandPlan } from './lazy-demand.js' import { crossJoinParentRoutes } from './parent-routes.js' import { queriesMatchForCaching } from './query-equivalence.js' import { @@ -41,7 +40,6 @@ import { getRoutedScalarMetadata, stripRouteMetadata, } from './route-metadata.js' -import type { LazyCollectionCallbacks } from './lazy-demand.js' import type { ValueIdentity } from '../equality-value-identity.js' import type { CompileQueryFn } from './index.js' import type { OrderByOptimizationInfo } from './order-by.js' @@ -64,7 +62,18 @@ import type { import type { QueryCache, QueryMapping, WindowOptions } from './types.js' import type { CollectionSubscription } from '../../collection/subscription.js' -export type { LazyCollectionCallbacks, LazyDemandPlan } from './lazy-demand.js' +export type LazyDemandPlan = { + id: string + path: Array + collectionId: string + initialKeys: Set +} + +/** Callbacks for managing lazy-loaded collections in optimized joins */ +export type LazyCollectionCallbacks = { + plans?: Array + setDemand?: (plan: LazyDemandPlan, keys: Set) => void +} type JoinInputValue = [ originalKey: unknown, @@ -72,6 +81,8 @@ type JoinInputValue = [ joinValue: unknown, ] +let nextLazyDemandPlanId = 0 + function parameterizeJoinInputByParentRoutes( input: KeyedStream, parentKeyStream: KeyedStream, @@ -180,6 +191,22 @@ function keyJoinInput( ] } +export function registerLazyDemandPlan( + callbacks: Record, + target: { sourceId: string; path: Array; collection: Collection }, + initialKeys: Set = new Set(), +): LazyDemandPlan { + const plan: LazyDemandPlan = { + id: `lazy-demand-${++nextLazyDemandPlanId}`, + path: target.path, + collectionId: target.collection.id, + initialKeys: new Set(initialKeys), + } + const state = (callbacks[target.sourceId] ??= {}) + ;(state.plans ??= []).push(plan) + return plan +} + /** * Processes all join clauses, applying lazy loading optimizations and maintaining * alias tracking for per-alias subscriptions (enables self-joins). diff --git a/packages/db/src/query/compiler/lazy-demand.ts b/packages/db/src/query/compiler/lazy-demand.ts deleted file mode 100644 index 708fe4d23e..0000000000 --- a/packages/db/src/query/compiler/lazy-demand.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Collection } from '../../collection/index.js' - -export type LazyDemandPlan = { - id: string - path: Array - collectionId: string - initialKeys: Set -} - -/** Callbacks for managing lazy-loaded collections in optimized joins */ -export type LazyCollectionCallbacks = { - plans?: Array - setDemand?: (plan: LazyDemandPlan, keys: Set) => void -} - -let nextLazyDemandPlanId = 0 - -export function registerLazyDemandPlan( - callbacks: Record, - target: { sourceId: string; path: Array; collection: Collection }, - initialKeys: Set = new Set(), -): LazyDemandPlan { - const plan: LazyDemandPlan = { - id: `lazy-demand-${++nextLazyDemandPlanId}`, - path: target.path, - collectionId: target.collection.id, - initialKeys: new Set(initialKeys), - } - const state = (callbacks[target.sourceId] ??= {}) - ;(state.plans ??= []).push(plan) - return plan -} diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index fcaf00861f..319c2f898f 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -45,18 +45,6 @@ export { export { groupKeys, type GroupKeys } from './builder/group-keys.js' export type { OperatorCallback } from './builder/types.js' -// Join operators -export { - leftJoin, - rightJoin, - innerJoin, - fullJoin, - joinOf, - type JoinCondition, - type JoinOperator, - type JoinType, -} from './builder/join-operators.js' - // Expression functions exports export { // Operators diff --git a/packages/db/src/query/ir.ts b/packages/db/src/query/ir.ts index 6ce00cd175..f3134a217d 100644 --- a/packages/db/src/query/ir.ts +++ b/packages/db/src/query/ir.ts @@ -10,7 +10,6 @@ import type { Collection, CollectionImpl } from '../collection/index.js' import type { CollectionOptionsIdentity } from '../collection-options.js' import type { NamespacedRow } from '../types' -import type { JoinRuntime } from './compiler/join-runtime.js' import type { Grouping } from './compiler/grouping.js' import type { IncludesRuntime } from './live/includes-runtime.js' @@ -55,9 +54,6 @@ export interface JoinClause { from: CollectionSourceRef | QueryRef type: `left` | `right` | `inner` | `outer` | `full` | `cross` on: BasicExpression - // The join operator value that built this clause supplies the compiler, - // so apps without joins never import it. - runtime: JoinRuntime } export type Where = diff --git a/packages/db/src/query/live/ARCHITECTURE.md b/packages/db/src/query/live/ARCHITECTURE.md index e37e819956..b483afc30a 100644 --- a/packages/db/src/query/live/ARCHITECTURE.md +++ b/packages/db/src/query/live/ARCHITECTURE.md @@ -109,11 +109,11 @@ operators and a few boundary adapters: Operator values carry their compilers, so an app bundles only the operators it imports. `include()`, `toArray()`, and `materialize()` carry the materializer -and the Collection-facade adapter (`includes-runtime.ts`); join operators carry -the join compiler, the optimizer's join pushdown, and public-key reduction -(`public-key-reduction.ts`), without the include materializer; aggregates and `groupKeys()` carry the grouping compiler. A query -that uses an operator always holds the value that carries it, so compilation -stays synchronous. +and the Collection-facade adapter (`includes-runtime.ts`); aggregates and +`groupKeys()` carry the grouping compiler. A query that uses an operator always +holds the value that carries it, so compilation stays synchronous. Joins are +core: the join compiler and public-key reduction (`public-key-reduction.ts`) +ship with every live query, without the include materializer. Queries without includes keep the original compiled pipeline and do not pay for facade state. The one exception is a joined query with a custom public-key diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index b761527b03..0ed502c208 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -14,6 +14,7 @@ import { runAllCallbacks } from '../../utils/callbacks.js' import { normalizeError } from '../../utils/error.js' import { createSourceRecord } from '../../utils/source-record.js' import { codedMessage, devBuild } from '../../error-message.js' +import { reducePublicKeys } from './public-key-reduction.js' import { CollectionSubscriber } from './collection-subscriber.js' import { getCollectionBuilder } from './collection-registry.js' import { LIVE_QUERY_INTERNAL } from './internal.js' @@ -28,7 +29,6 @@ import { } from './utils.js' import type { LiveQueryInternalUtils } from './internal.js' import type { IncludesRuntime } from './includes-runtime.js' -import type { BasicExpression, JoinClause, QueryIR } from '../ir.js' import type { BucketFacadeAdapter } from './bucket-facade-adapter.js' import type { materializeCompilation } from './materialized-pipeline.js' import type { WindowOptions } from '../compiler/index.js' @@ -46,6 +46,7 @@ import type { UtilsRecord, } from '../../types.js' import type { Context, GetResult } from '../builder/types.js' +import type { BasicExpression, QueryIR } from '../ir.js' import type { LazyCollectionCallbacks } from '../compiler/joins.js' import type { Deferred } from '../../deferred.js' import type { @@ -240,30 +241,34 @@ export class CollectionConfigBuilder< } /** - * Returns a join clause from the query or any of its subqueries + * Recursively checks if a query or any of its subqueries contains joins */ - private findJoinClause(query: QueryIR): JoinClause | undefined { - if (query.join && query.join.length > 0) return query.join[0] - const from = query.from - const nested = - from.type === `queryRef` - ? [from.query] - : from.type === `unionFrom` - ? from.sources.flatMap((source) => - source.type === `queryRef` ? [source.query] : [], - ) - : from.type === `unionAll` - ? from.queries - : [] - for (const subquery of nested) { - const joinClause = this.findJoinClause(subquery) - if (joinClause) return joinClause + private hasJoins(query: QueryIR): boolean { + // Check if this query has joins + if (query.join && query.join.length > 0) { + return true } - return undefined - } - private hasJoins(query: QueryIR): boolean { - return this.findJoinClause(query) !== undefined + // Recursively check subqueries in the from clause + if (query.from.type === `queryRef`) { + if (this.hasJoins(query.from.query)) { + return true + } + } else if (query.from.type === `unionFrom`) { + for (const source of query.from.sources) { + if (source.type === `queryRef` && this.hasJoins(source.query)) { + return true + } + } + } else if (query.from.type === `unionAll`) { + for (const branch of query.from.queries) { + if (this.hasJoins(branch)) { + return true + } + } + } + + return false } getConfig(): CollectionConfigSingleRowOption & { @@ -910,14 +915,13 @@ export class CollectionConfigBuilder< ) // Includes bring the materializer. A joined query with a custom key - // reduces its public keys, so its join operator brings the reduction. + // reduces its public keys without it. const includesRuntime = compilation.includes?.[0]?.runtime - const joinClause = this.findJoinClause(this.query) const getKey = this.config.getKey const materialized = includesRuntime ? includesRuntime.materializeCompilation(compilation, getKey) - : getKey && joinClause - ? joinClause.runtime.reducePublicKeys(compilation, getKey) + : getKey && this.hasJoins(this.query) + ? reducePublicKeys(compilation, getKey) : { pipeline: compilation.pipeline, facades: undefined } this.pipelineCache = materialized.pipeline this.sourceWhereClausesCache = compilation.sourceWhereClauses diff --git a/packages/db/src/query/live/includes-runtime.ts b/packages/db/src/query/live/includes-runtime.ts index c390a157c7..9da18d47e9 100644 --- a/packages/db/src/query/live/includes-runtime.ts +++ b/packages/db/src/query/live/includes-runtime.ts @@ -3,8 +3,7 @@ import { BucketFacadeAdapter } from './bucket-facade-adapter.js' /** * The include materializer. `include()`, `toArray()`, and `materialize()` - * carry it onto the query IR, so other apps do not bundle it. Join operators - * carry only public-key reduction. + * carry it onto the query IR, so other apps do not bundle it. */ export const includesRuntime = { materializeCompilation, diff --git a/packages/db/src/query/live/public-key-reduction.ts b/packages/db/src/query/live/public-key-reduction.ts index 8065b9a972..e75ce7ee01 100644 --- a/packages/db/src/query/live/public-key-reduction.ts +++ b/packages/db/src/query/live/public-key-reduction.ts @@ -8,7 +8,7 @@ import type { CompilationResult } from '../compiler/index.js' import type { ResultStream } from '../../types.js' // Public-key reduction. A joined query with a custom key needs only this -// part of materialization, so join operators carry it without the include +// part of materialization, so it ships in core without the include // materializer. export type ResultTuple = [ diff --git a/packages/db/src/query/optimizer-joins.ts b/packages/db/src/query/optimizer-joins.ts deleted file mode 100644 index 39a3764664..0000000000 --- a/packages/db/src/query/optimizer-joins.ts +++ /dev/null @@ -1,494 +0,0 @@ -import { - Func, - PropRef, - QueryRef as QueryRefClass, - UnionAll as UnionAllClass, - UnionFrom as UnionFromClass, - collectPropRefs, - createResidualWhere, - getWhereExpression, - isResidualWhere, -} from './ir.js' -import { findAggregate } from './compiler/find-aggregate.js' -import { - combineWithAnd, - getFirstFromAlias, - getLocalBindingIds, - getNullableJoinSources, - splitAndClausesRecursive, -} from './optimizer.js' -import type { GroupedWhereClauses } from './optimizer.js' -import type { - BasicExpression, - CollectionSourceRef, - From, - QueryIR, - Select, - Where, -} from './ir.js' - -// Predicate pushdown for queries with joins. Join operator values supply this -// to the optimizer, so apps without joins never import it. - -/** - * Step 4: Apply optimizations by lifting single-source clauses into subqueries. - * - * Creates a new QueryIR with single-source WHERE clauses moved to subqueries - * that wrap the original table references. This ensures immutability and prevents - * infinite recursion issues. - * - * @param query - Original QueryIR to optimize - * @param groupedClauses - WHERE clauses grouped by optimization strategy - * @returns New QueryIR with optimizations applied - */ -export function applyOptimizations( - query: QueryIR, - groupedClauses: GroupedWhereClauses, -): QueryIR { - // Track which single-source clauses were actually optimized - const actuallyOptimized = new Set() - - // Determine which source aliases are on the nullable side of outer joins. - const nullableSources = getNullableJoinSources(query) - - // Build a filtered copy of singleSource that excludes nullable-side clauses. - // Pushing a WHERE clause into the nullable side's subquery pre-filters the - // data before the join, converting "matched but WHERE-excluded" rows into - // "unmatched" outer-join rows. These are indistinguishable from genuinely - // unmatched rows, so the residual WHERE cannot correct the result. - const pushableSingleSource = new Map>() - for (const [source, clause] of groupedClauses.singleSource) { - if (!nullableSources.has(source)) { - pushableSingleSource.set(source, clause) - } - } - - // Optimize the main FROM clause and track what was optimized - const optimizedFrom = optimizeFromWithTracking( - query.from, - pushableSingleSource, - actuallyOptimized, - ) - - // Optimize JOIN clauses and track what was optimized - const optimizedJoins = query.join - ? query.join.map((joinClause) => ({ - ...joinClause, - from: optimizeFromWithTracking( - joinClause.from, - pushableSingleSource, - actuallyOptimized, - ) as CollectionSourceRef | QueryRefClass, - })) - : undefined - - // Keep regular and residual clauses separate so neither loses its marker. - const remainingWhereClauses: Array = [] - const residualWhereClauses = query.where?.filter(isResidualWhere) ?? [] - - // Add multi-source clauses - if (groupedClauses.multiSource) { - remainingWhereClauses.push(groupedClauses.multiSource) - } - - // Determine if we need residual clauses (when query has outer JOINs) - const hasOuterJoins = nullableSources.size > 0 - - // Add single-source clauses - for (const [source, clause] of groupedClauses.singleSource) { - if (!actuallyOptimized.has(source)) { - // Wasn't optimized at all - keep as regular WHERE clause - remainingWhereClauses.push(clause) - } else if (hasOuterJoins) { - // Was optimized AND query has outer JOINs - keep as residual WHERE clause - residualWhereClauses.push(createResidualWhere(clause)) - } - // If optimized and no outer JOINs - don't keep (original behavior) - } - - // Combine within each group to avoid extra filters and nested ANDs. - const combineRemaining = (clauses: Array): Where | undefined => - clauses.length > 1 - ? combineWithAnd( - clauses.flatMap((clause) => - splitAndClausesRecursive(getWhereExpression(clause)), - ), - ) - : clauses[0] - const regularWhere = combineRemaining(remainingWhereClauses) - const residualWhere = combineRemaining(residualWhereClauses) - const finalWhere: Array = [ - ...(regularWhere ? [regularWhere] : []), - ...(residualWhere - ? [createResidualWhere(getWhereExpression(residualWhere))] - : []), - ] - - // Preserve untouched query options while replacing the optimized clauses. - const optimizedQuery: QueryIR = { - ...copyClauseArrays(query), - from: optimizedFrom, - join: optimizedJoins, - where: finalWhere.length > 0 ? finalWhere : [], - } - - return optimizedQuery -} - -/** - * Helper function to create a deep copy of a QueryIR object for immutability. - * - * This ensures that all optimizations create new objects rather than modifying - * existing ones, preventing infinite recursion and shared reference issues. - * - * @param query - QueryIR to deep copy - * @returns New QueryIR object with all nested objects copied - */ -function deepCopyQuery(query: QueryIR): QueryIR { - return { - ...copyClauseArrays(query), - from: deepCopyFrom(query.from), - join: query.join - ? query.join.map((joinClause) => ({ - ...joinClause, - from: deepCopyFrom(joinClause.from) as - CollectionSourceRef | QueryRefClass, - })) - : undefined, - where: query.where ? [...query.where] : undefined, - } -} - -/** - * Helper function to optimize a FROM clause while tracking what was actually optimized. - * - * @param from - FROM clause to optimize - * @param singleSourceClauses - Map of source aliases to their WHERE clauses - * @param actuallyOptimized - Set to track which sources were actually optimized - * @returns New FROM clause, potentially wrapped in a subquery - */ -function optimizeFromWithTracking( - from: From, - singleSourceClauses: Map>, - actuallyOptimized: Set, -): From { - if (from.type === `unionFrom`) { - return new UnionFromClass( - from.sources.map( - (source) => - optimizeFromWithTracking( - source, - singleSourceClauses, - actuallyOptimized, - ) as CollectionSourceRef | QueryRefClass, - ), - ) - } - - if (from.type === `unionAll`) { - return new UnionAllClass( - from.queries.map((branch) => deepCopyQuery(branch)), - ) - } - - const whereClause = singleSourceClauses.get(from.alias) - - if (!whereClause) { - // No optimization needed. Keep the source ref itself: its sourceId is - // the compiled input identity, and a copy would lose it. - if (from.type === `collectionRef` || from.type === `descriptorRef`) { - return from - } - // Must be queryRef due to type system - return new QueryRefClass( - deepCopyQuery(from.query), - from.alias, - from.bindingId, - ) - } - - if (from.type === `collectionRef` || from.type === `descriptorRef`) { - // Create a new subquery with the WHERE clause for the collection - // This is always safe since we're creating a new subquery - const subQuery: QueryIR = { - from, - where: [whereClause], - } - actuallyOptimized.add(from.alias) // Mark as successfully optimized - return new QueryRefClass(subQuery, from.alias, from.bindingId) - } - - // SAFETY CHECK: Only check safety when pushing WHERE clauses into existing subqueries - // We need to be careful about pushing WHERE clauses into subqueries that already have - // aggregates, HAVING, or ORDER BY + LIMIT since that could change their semantics - if (!isSafeToPushIntoExistingSubquery(from.query, whereClause, from.alias)) { - // Return a copy without optimization to maintain immutability - // Do NOT mark as optimized since we didn't actually optimize it - return new QueryRefClass( - deepCopyQuery(from.query), - from.alias, - from.bindingId, - ) - } - - // Skip pushdown when a clause references a field that only exists via a renamed - // projection inside the subquery; leaving it outside preserves the alias mapping. - if (referencesAliasWithRemappedSelect(from.query, whereClause, from.alias)) { - return new QueryRefClass( - deepCopyQuery(from.query), - from.alias, - from.bindingId, - ) - } - - // Add the WHERE clause to the existing subquery - // Create a deep copy to ensure immutability - const existingWhere = from.query.where || [] - const remappedWhere = remapWhereForSubquery( - from.query, - whereClause, - from.alias, - ) - if (remappedWhere === undefined) { - return new QueryRefClass( - deepCopyQuery(from.query), - from.alias, - from.bindingId, - ) - } - const optimizedSubQuery: QueryIR = { - ...deepCopyQuery(from.query), - where: [...existingWhere, remappedWhere], - } - actuallyOptimized.add(from.alias) // Mark as successfully optimized - return new QueryRefClass(optimizedSubQuery, from.alias, from.bindingId) -} - -/** - * Rewrites references to an outer QueryRef alias so a pushed predicate can be - * evaluated inside the subquery's namespace. Pass-through SELECT fields use - * their projected source path; unprojected rows use the first source alias. - */ -function remapWhereForSubquery( - subquery: QueryIR, - whereClause: BasicExpression, - outerAlias: string, -): BasicExpression | undefined { - const firstFromAlias = getFirstFromAlias(subquery) - if (firstFromAlias === undefined) return undefined - - const remapExpression = (expression: BasicExpression): BasicExpression => { - if (expression instanceof PropRef) { - if (expression.path[0] !== outerAlias) return expression - - const field = expression.path[1] - const projected = field ? subquery.select?.[field] : undefined - const hasNamespacedResult = - subquery.join !== undefined || subquery.from.type === `unionFrom` - const innerPath = - projected instanceof PropRef - ? [...projected.path, ...expression.path.slice(2)] - : hasNamespacedResult - ? expression.path.slice(1) - : [firstFromAlias, ...expression.path.slice(1)] - - return new PropRef(innerPath) - } - - if (expression instanceof Func) { - return new Func(expression.name, expression.args.map(remapExpression)) - } - - return expression - } - - return remapExpression(whereClause) as BasicExpression -} - -function isSafeToPushIntoExistingSubquery( - query: QueryIR, - whereClause: BasicExpression, - outerAlias: string, -): boolean { - return !( - query.distinct || - (query.select && - (findAggregate(query.select) !== undefined || - whereReferencesComputedSelectFields( - query.select, - whereClause, - outerAlias, - ))) || - query.groupBy?.length || - query.having?.length || - (query.orderBy?.length && - (query.limit !== undefined || query.offset !== undefined)) || - query.fnSelect || - query.fnWhere?.length || - query.fnHaving?.length - ) -} - -/** - * Determines whether the provided WHERE clause references fields that are - * computed by a subquery SELECT rather than pass-through properties. - * - * If true, pushing the WHERE clause into the subquery could change semantics - * (since computed fields do not necessarily exist at the subquery input level), - * so predicate pushdown must be avoided. - * - * @param select - The subquery SELECT map - * @param whereClause - The WHERE expression to analyze - * @param outerAlias - The alias of the subquery in the outer query - * @returns True if WHERE references computed fields, otherwise false - */ -function whereReferencesComputedSelectFields( - select: Select, - whereClause: BasicExpression, - outerAlias: string, -): boolean { - // Build a set of computed field names at the top-level of the subquery output - const computed = new Set() - for (const [key, value] of Object.entries(select)) { - if (key.startsWith(`__SPREAD_SENTINEL__`)) continue - if (value instanceof PropRef) continue - // Nested object or non-PropRef expression counts as computed - computed.add(key) - } - - const refs = collectPropRefs(whereClause) - - for (const ref of refs) { - const path = (ref as any).path as Array - if (!Array.isArray(path) || path.length < 2) continue - const alias = path[0] - const field = path[1] as string - if (alias !== outerAlias) continue - if (computed.has(field)) return true - } - return false -} - -/** - * Detects whether a WHERE clause references the subquery alias through fields that - * are re-exposed under different names (renamed SELECT projections or fnSelect output). - * In those cases we keep the clause at the outer level to avoid alias remapping bugs. - * TODO: in future we should handle this by rewriting the clause to use the subquery's - * internal field references, but it likely needs a wider refactor to do cleanly. - */ -function referencesAliasWithRemappedSelect( - subquery: QueryIR, - whereClause: BasicExpression, - outerAlias: string, -): boolean { - const refs = collectPropRefs(whereClause) - // Only care about clauses that actually reference the outer alias. - if (refs.every((ref) => ref.path[0] !== outerAlias)) { - return false - } - - // fnSelect always rewrites the row shape, so alias-safe pushdown is impossible. - if (subquery.fnSelect) { - return true - } - - const select = subquery.select - // Without an explicit SELECT the clause still refers to the original collection. - if (!select) { - return false - } - const hasSpreadProjection = Object.keys(select).some((key) => - key.startsWith(`__SPREAD_SENTINEL__`), - ) - const localBindings = getLocalBindingIds(subquery) - - for (const ref of refs) { - const path = ref.path - // Need at least alias + field to matter. - if (path.length < 2) continue - if (path[0] !== outerAlias) continue - - const projected = select[path[1]!] - // A spread-selected field has no direct projection entry to remap. - // Keep its predicate outside rather than guessing its input source. - if (!projected) { - if (hasSpreadProjection) return true - continue - } - - // Non-PropRef projections are computed values; cannot push down. - if (!(projected instanceof PropRef)) { - return true - } - - // A selected ancestor field depends on the parent route. Moving its - // predicate into this source would read a same-named child field instead. - if ( - projected.bindingId !== undefined && - !localBindings.has(projected.bindingId) - ) { - return true - } - - // If the projection is just the alias (whole row) without a specific field, - // we can't verify whether the field we're referencing is being preserved or remapped. - if (projected.path.length < 2) { - return true - } - - const [innerAlias, innerField] = projected.path - - // Safe only when the projection points straight back to the same alias or the - // underlying source alias and preserves the field name. - const firstFromAlias = getFirstFromAlias(subquery) - if (innerAlias !== outerAlias && innerAlias !== firstFromAlias) { - return true - } - - if (innerField !== path[1]) { - return true - } - } - - return false -} - -/** A copy of the query whose clause arrays are new arrays. */ -function copyClauseArrays(query: QueryIR): QueryIR { - return { - ...query, - groupBy: query.groupBy ? [...query.groupBy] : undefined, - having: query.having ? [...query.having] : undefined, - orderBy: query.orderBy ? [...query.orderBy] : undefined, - fnWhere: query.fnWhere ? [...query.fnWhere] : undefined, - fnHaving: query.fnHaving ? [...query.fnHaving] : undefined, - } -} - -function deepCopyFrom(from: From): From { - // Share source refs: their SourceId is the compiled input identity. - if (from.type === `collectionRef` || from.type === `descriptorRef`) { - return from - } - - if (from.type === `queryRef`) { - return new QueryRefClass( - deepCopyQuery(from.query), - from.alias, - from.bindingId, - ) - } - - if (from.type === `unionAll`) { - return new UnionAllClass( - from.queries.map((branch) => deepCopyQuery(branch)), - ) - } - - return new UnionFromClass( - from.sources.map( - (source: CollectionSourceRef | QueryRefClass) => - deepCopyFrom(source) as any, - ), - ) -} diff --git a/packages/db/src/query/optimizer.ts b/packages/db/src/query/optimizer.ts index 38e8489b23..d214a737e8 100644 --- a/packages/db/src/query/optimizer.ts +++ b/packages/db/src/query/optimizer.ts @@ -60,7 +60,7 @@ * ```javascript * query * .from({ users: usersCollection }) - * .join(leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))) + * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id)) * .where(({users}) => eq(users.department_id, 1)) * .where(({posts}) => gt(posts.views, 100)) * .where(({users, posts}) => eq(users.id, posts.author_id)) @@ -74,11 +74,11 @@ * .from({ users: usersCollection }) * .where(({users}) => eq(users.department_id, 1)) * }) - * .join(leftJoin({ + * .join({ * posts: subquery * .from({ posts: postsCollection }) * .where(({posts}) => gt(posts.views, 100)) - * }, ({users, posts}) => eq(users.id, posts.user_id))) + * }, ({users, posts}) => eq(users.id, posts.user_id)) * .where(({users, posts}) => eq(users.id, posts.author_id)) * ``` * @@ -87,7 +87,7 @@ * ```javascript * query * .from({ users: usersCollection }) - * .join(leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))) + * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id)) * .where(({users}) => eq(users.department_id, 1)) * .groupBy(['users.department_id']) * .select({ count: agg('count', '*') }) @@ -101,7 +101,7 @@ * .from({ users: usersCollection }) * .where(({users}) => eq(users.department_id, 1)) * }) - * .join(leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))) + * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id)) * .groupBy(['users.department_id']) * .select({ count: agg('count', '*') }) * ``` @@ -122,11 +122,15 @@ import { deepEquals } from '../utils.js' import { CannotCombineEmptyExpressionListError } from '../errors.js' +import { findAggregate } from './compiler/find-aggregate.js' import { Func, + PropRef, QueryRef as QueryRefClass, UnionAll as UnionAllClass, UnionFrom as UnionFromClass, + collectPropRefs, + createResidualWhere, getFromSources, getWhereExpression, isResidualWhere, @@ -136,6 +140,7 @@ import type { CollectionSourceRef, From, QueryIR, + Select, Where, } from './ir.js' @@ -336,7 +341,7 @@ function isCollectionReference(query: QueryIR, sourceAlias: string): boolean { * unmatched outer-join rows (with the nullable side set to undefined) and * incorrectly survive residual filtering. */ -export function getNullableJoinSources(query: QueryIR): Set { +function getNullableJoinSources(query: QueryIR): Set { const nullable = new Set() if (query.join) { const leftAliases = new Set( @@ -438,9 +443,8 @@ function applySingleLevelOptimization(query: QueryIR): QueryIR { // Step 3: Group clauses by single-source vs multi-source const groupedClauses = groupWhereClauses(analyzedClauses) - // Step 4: The join operator supplies the pushdown that lifts single-source - // clauses into subqueries, so apps without joins never import it. - return query.join[0]!.runtime.optimize(query, groupedClauses) + // Step 4: Apply optimizations by lifting single-source clauses into subqueries + return applyOptimizations(query, groupedClauses) } /** @@ -570,7 +574,7 @@ function splitAndClauses( } // Helper function for recursive splitting of BasicExpression arrays -export function splitAndClausesRecursive( +function splitAndClausesRecursive( clause: BasicExpression, ): Array> { if (clause.type === `func` && clause.name === `and`) { @@ -610,7 +614,7 @@ export function splitAndClausesRecursive( * // eq(special.id, 5) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown) * ``` */ -export function getLocalBindingIds(query: QueryIR): Set { +function getLocalBindingIds(query: QueryIR): Set { return new Set([ ...getFromSources(query.from).map((source) => source.bindingId), ...(query.join?.map(({ from }) => from.bindingId) ?? []), @@ -732,6 +736,176 @@ function groupWhereClauses( } } +/** + * Step 4: Apply optimizations by lifting single-source clauses into subqueries. + * + * Creates a new QueryIR with single-source WHERE clauses moved to subqueries + * that wrap the original table references. This ensures immutability and prevents + * infinite recursion issues. + * + * @param query - Original QueryIR to optimize + * @param groupedClauses - WHERE clauses grouped by optimization strategy + * @returns New QueryIR with optimizations applied + */ +function applyOptimizations( + query: QueryIR, + groupedClauses: GroupedWhereClauses, +): QueryIR { + // Track which single-source clauses were actually optimized + const actuallyOptimized = new Set() + + // Determine which source aliases are on the nullable side of outer joins. + const nullableSources = getNullableJoinSources(query) + + // Build a filtered copy of singleSource that excludes nullable-side clauses. + // Pushing a WHERE clause into the nullable side's subquery pre-filters the + // data before the join, converting "matched but WHERE-excluded" rows into + // "unmatched" outer-join rows. These are indistinguishable from genuinely + // unmatched rows, so the residual WHERE cannot correct the result. + const pushableSingleSource = new Map>() + for (const [source, clause] of groupedClauses.singleSource) { + if (!nullableSources.has(source)) { + pushableSingleSource.set(source, clause) + } + } + + // Optimize the main FROM clause and track what was optimized + const optimizedFrom = optimizeFromWithTracking( + query.from, + pushableSingleSource, + actuallyOptimized, + ) + + // Optimize JOIN clauses and track what was optimized + const optimizedJoins = query.join + ? query.join.map((joinClause) => ({ + ...joinClause, + from: optimizeFromWithTracking( + joinClause.from, + pushableSingleSource, + actuallyOptimized, + ) as CollectionSourceRef | QueryRefClass, + })) + : undefined + + // Keep regular and residual clauses separate so neither loses its marker. + const remainingWhereClauses: Array = [] + const residualWhereClauses = query.where?.filter(isResidualWhere) ?? [] + + // Add multi-source clauses + if (groupedClauses.multiSource) { + remainingWhereClauses.push(groupedClauses.multiSource) + } + + // Determine if we need residual clauses (when query has outer JOINs) + const hasOuterJoins = nullableSources.size > 0 + + // Add single-source clauses + for (const [source, clause] of groupedClauses.singleSource) { + if (!actuallyOptimized.has(source)) { + // Wasn't optimized at all - keep as regular WHERE clause + remainingWhereClauses.push(clause) + } else if (hasOuterJoins) { + // Was optimized AND query has outer JOINs - keep as residual WHERE clause + residualWhereClauses.push(createResidualWhere(clause)) + } + // If optimized and no outer JOINs - don't keep (original behavior) + } + + // Combine within each group to avoid extra filters and nested ANDs. + const combineRemaining = (clauses: Array): Where | undefined => + clauses.length > 1 + ? combineWithAnd( + clauses.flatMap((clause) => + splitAndClausesRecursive(getWhereExpression(clause)), + ), + ) + : clauses[0] + const regularWhere = combineRemaining(remainingWhereClauses) + const residualWhere = combineRemaining(residualWhereClauses) + const finalWhere: Array = [ + ...(regularWhere ? [regularWhere] : []), + ...(residualWhere + ? [createResidualWhere(getWhereExpression(residualWhere))] + : []), + ] + + // Preserve untouched query options while replacing the optimized clauses. + const optimizedQuery: QueryIR = { + ...copyClauseArrays(query), + from: optimizedFrom, + join: optimizedJoins, + where: finalWhere.length > 0 ? finalWhere : [], + } + + return optimizedQuery +} + +/** + * Helper function to create a deep copy of a QueryIR object for immutability. + * + * This ensures that all optimizations create new objects rather than modifying + * existing ones, preventing infinite recursion and shared reference issues. + * + * @param query - QueryIR to deep copy + * @returns New QueryIR object with all nested objects copied + */ +function deepCopyQuery(query: QueryIR): QueryIR { + return { + ...copyClauseArrays(query), + from: deepCopyFrom(query.from), + join: query.join + ? query.join.map((joinClause) => ({ + type: joinClause.type, + on: joinClause.on, + from: deepCopyFrom(joinClause.from) as + CollectionSourceRef | QueryRefClass, + })) + : undefined, + where: query.where ? [...query.where] : undefined, + } +} + +/** A copy of the query whose clause arrays are new arrays. */ +function copyClauseArrays(query: QueryIR): QueryIR { + return { + ...query, + groupBy: query.groupBy ? [...query.groupBy] : undefined, + having: query.having ? [...query.having] : undefined, + orderBy: query.orderBy ? [...query.orderBy] : undefined, + fnWhere: query.fnWhere ? [...query.fnWhere] : undefined, + fnHaving: query.fnHaving ? [...query.fnHaving] : undefined, + } +} + +function deepCopyFrom(from: From): From { + // Share source refs: their SourceId is the compiled input identity. + if (from.type === `collectionRef` || from.type === `descriptorRef`) { + return from + } + + if (from.type === `queryRef`) { + return new QueryRefClass( + deepCopyQuery(from.query), + from.alias, + from.bindingId, + ) + } + + if (from.type === `unionAll`) { + return new UnionAllClass( + from.queries.map((branch) => deepCopyQuery(branch)), + ) + } + + return new UnionFromClass( + from.sources.map( + (source: CollectionSourceRef | QueryRefClass) => + deepCopyFrom(source) as any, + ), + ) +} + function optimizeNestedFrom(from: From): From { if (from.type === `queryRef`) { return new QueryRefClass( @@ -756,10 +930,304 @@ function optimizeNestedFrom(from: From): From { return from } -export function getFirstFromAlias(query: QueryIR): string | undefined { +function getFirstFromAlias(query: QueryIR): string | undefined { return getFromSources(query.from)[0]?.alias } +/** + * Helper function to optimize a FROM clause while tracking what was actually optimized. + * + * @param from - FROM clause to optimize + * @param singleSourceClauses - Map of source aliases to their WHERE clauses + * @param actuallyOptimized - Set to track which sources were actually optimized + * @returns New FROM clause, potentially wrapped in a subquery + */ +function optimizeFromWithTracking( + from: From, + singleSourceClauses: Map>, + actuallyOptimized: Set, +): From { + if (from.type === `unionFrom`) { + return new UnionFromClass( + from.sources.map( + (source) => + optimizeFromWithTracking( + source, + singleSourceClauses, + actuallyOptimized, + ) as CollectionSourceRef | QueryRefClass, + ), + ) + } + + if (from.type === `unionAll`) { + return new UnionAllClass( + from.queries.map((branch) => deepCopyQuery(branch)), + ) + } + + const whereClause = singleSourceClauses.get(from.alias) + + if (!whereClause) { + // No optimization needed. Keep the source ref itself: its sourceId is + // the compiled input identity, and a copy would lose it. + if (from.type === `collectionRef` || from.type === `descriptorRef`) { + return from + } + // Must be queryRef due to type system + return new QueryRefClass( + deepCopyQuery(from.query), + from.alias, + from.bindingId, + ) + } + + if (from.type === `collectionRef` || from.type === `descriptorRef`) { + // Create a new subquery with the WHERE clause for the collection + // This is always safe since we're creating a new subquery + const subQuery: QueryIR = { + from, + where: [whereClause], + } + actuallyOptimized.add(from.alias) // Mark as successfully optimized + return new QueryRefClass(subQuery, from.alias, from.bindingId) + } + + // SAFETY CHECK: Only check safety when pushing WHERE clauses into existing subqueries + // We need to be careful about pushing WHERE clauses into subqueries that already have + // aggregates, HAVING, or ORDER BY + LIMIT since that could change their semantics + if (!isSafeToPushIntoExistingSubquery(from.query, whereClause, from.alias)) { + // Return a copy without optimization to maintain immutability + // Do NOT mark as optimized since we didn't actually optimize it + return new QueryRefClass( + deepCopyQuery(from.query), + from.alias, + from.bindingId, + ) + } + + // Skip pushdown when a clause references a field that only exists via a renamed + // projection inside the subquery; leaving it outside preserves the alias mapping. + if (referencesAliasWithRemappedSelect(from.query, whereClause, from.alias)) { + return new QueryRefClass( + deepCopyQuery(from.query), + from.alias, + from.bindingId, + ) + } + + // Add the WHERE clause to the existing subquery + // Create a deep copy to ensure immutability + const existingWhere = from.query.where || [] + const remappedWhere = remapWhereForSubquery( + from.query, + whereClause, + from.alias, + ) + if (remappedWhere === undefined) { + return new QueryRefClass( + deepCopyQuery(from.query), + from.alias, + from.bindingId, + ) + } + const optimizedSubQuery: QueryIR = { + ...deepCopyQuery(from.query), + where: [...existingWhere, remappedWhere], + } + actuallyOptimized.add(from.alias) // Mark as successfully optimized + return new QueryRefClass(optimizedSubQuery, from.alias, from.bindingId) +} + +/** + * Rewrites references to an outer QueryRef alias so a pushed predicate can be + * evaluated inside the subquery's namespace. Pass-through SELECT fields use + * their projected source path; unprojected rows use the first source alias. + */ +function remapWhereForSubquery( + subquery: QueryIR, + whereClause: BasicExpression, + outerAlias: string, +): BasicExpression | undefined { + const firstFromAlias = getFirstFromAlias(subquery) + if (firstFromAlias === undefined) return undefined + + const remapExpression = (expression: BasicExpression): BasicExpression => { + if (expression instanceof PropRef) { + if (expression.path[0] !== outerAlias) return expression + + const field = expression.path[1] + const projected = field ? subquery.select?.[field] : undefined + const hasNamespacedResult = + subquery.join !== undefined || subquery.from.type === `unionFrom` + const innerPath = + projected instanceof PropRef + ? [...projected.path, ...expression.path.slice(2)] + : hasNamespacedResult + ? expression.path.slice(1) + : [firstFromAlias, ...expression.path.slice(1)] + + return new PropRef(innerPath) + } + + if (expression instanceof Func) { + return new Func(expression.name, expression.args.map(remapExpression)) + } + + return expression + } + + return remapExpression(whereClause) as BasicExpression +} + +function isSafeToPushIntoExistingSubquery( + query: QueryIR, + whereClause: BasicExpression, + outerAlias: string, +): boolean { + return !( + query.distinct || + (query.select && + (findAggregate(query.select) !== undefined || + whereReferencesComputedSelectFields( + query.select, + whereClause, + outerAlias, + ))) || + query.groupBy?.length || + query.having?.length || + (query.orderBy?.length && + (query.limit !== undefined || query.offset !== undefined)) || + query.fnSelect || + query.fnWhere?.length || + query.fnHaving?.length + ) +} + +/** + * Determines whether the provided WHERE clause references fields that are + * computed by a subquery SELECT rather than pass-through properties. + * + * If true, pushing the WHERE clause into the subquery could change semantics + * (since computed fields do not necessarily exist at the subquery input level), + * so predicate pushdown must be avoided. + * + * @param select - The subquery SELECT map + * @param whereClause - The WHERE expression to analyze + * @param outerAlias - The alias of the subquery in the outer query + * @returns True if WHERE references computed fields, otherwise false + */ +function whereReferencesComputedSelectFields( + select: Select, + whereClause: BasicExpression, + outerAlias: string, +): boolean { + // Build a set of computed field names at the top-level of the subquery output + const computed = new Set() + for (const [key, value] of Object.entries(select)) { + if (key.startsWith(`__SPREAD_SENTINEL__`)) continue + if (value instanceof PropRef) continue + // Nested object or non-PropRef expression counts as computed + computed.add(key) + } + + const refs = collectPropRefs(whereClause) + + for (const ref of refs) { + const path = (ref as any).path as Array + if (!Array.isArray(path) || path.length < 2) continue + const alias = path[0] + const field = path[1] as string + if (alias !== outerAlias) continue + if (computed.has(field)) return true + } + return false +} + +/** + * Detects whether a WHERE clause references the subquery alias through fields that + * are re-exposed under different names (renamed SELECT projections or fnSelect output). + * In those cases we keep the clause at the outer level to avoid alias remapping bugs. + * TODO: in future we should handle this by rewriting the clause to use the subquery's + * internal field references, but it likely needs a wider refactor to do cleanly. + */ +function referencesAliasWithRemappedSelect( + subquery: QueryIR, + whereClause: BasicExpression, + outerAlias: string, +): boolean { + const refs = collectPropRefs(whereClause) + // Only care about clauses that actually reference the outer alias. + if (refs.every((ref) => ref.path[0] !== outerAlias)) { + return false + } + + // fnSelect always rewrites the row shape, so alias-safe pushdown is impossible. + if (subquery.fnSelect) { + return true + } + + const select = subquery.select + // Without an explicit SELECT the clause still refers to the original collection. + if (!select) { + return false + } + const hasSpreadProjection = Object.keys(select).some((key) => + key.startsWith(`__SPREAD_SENTINEL__`), + ) + const localBindings = getLocalBindingIds(subquery) + + for (const ref of refs) { + const path = ref.path + // Need at least alias + field to matter. + if (path.length < 2) continue + if (path[0] !== outerAlias) continue + + const projected = select[path[1]!] + // A spread-selected field has no direct projection entry to remap. + // Keep its predicate outside rather than guessing its input source. + if (!projected) { + if (hasSpreadProjection) return true + continue + } + + // Non-PropRef projections are computed values; cannot push down. + if (!(projected instanceof PropRef)) { + return true + } + + // A selected ancestor field depends on the parent route. Moving its + // predicate into this source would read a same-named child field instead. + if ( + projected.bindingId !== undefined && + !localBindings.has(projected.bindingId) + ) { + return true + } + + // If the projection is just the alias (whole row) without a specific field, + // we can't verify whether the field we're referencing is being preserved or remapped. + if (projected.path.length < 2) { + return true + } + + const [innerAlias, innerField] = projected.path + + // Safe only when the projection points straight back to the same alias or the + // underlying source alias and preserves the field name. + const firstFromAlias = getFirstFromAlias(subquery) + if (innerAlias !== outerAlias && innerAlias !== firstFromAlias) { + return true + } + + if (innerField !== path[1]) { + return true + } + } + + return false +} + /** * Helper function to combine multiple expressions with AND. * @@ -770,7 +1238,7 @@ export function getFirstFromAlias(query: QueryIR): string | undefined { * @returns Single expression representing the AND combination * @throws Error if the expressions array is empty */ -export function combineWithAnd( +function combineWithAnd( expressions: Array>, ): BasicExpression { if (expressions.length === 0) { diff --git a/packages/db/tests/collection-auto-index.test.ts b/packages/db/tests/collection-auto-index.test.ts index 1a1b35400b..620affc7e1 100644 --- a/packages/db/tests/collection-auto-index.test.ts +++ b/packages/db/tests/collection-auto-index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { createLiveQueryCollection, leftJoin } from '../src' +import { createLiveQueryCollection } from '../src' import { CollectionConfigurationError } from '../src/errors' import { createCollection } from '../src/collection/index.js' import { @@ -583,9 +583,9 @@ describe(`Collection Auto-Indexing`, () => { q .from({ item: leftCollection }) .join( - leftJoin({ other: rightCollection }, ({ item, other }: any) => - eq(item.id, other.id2), - ), + { other: rightCollection }, + ({ item, other }: any) => eq(item.id, other.id2), + `left`, ) .select(({ item, other }: any) => ({ id: item.id, @@ -697,17 +697,16 @@ describe(`Collection Auto-Indexing`, () => { q .from({ item: leftCollection }) .join( - leftJoin( - { - other: q - .from({ other: rightCollection }) - .select(({ other }: any) => ({ - id2: other.id2, - name: other.name, - })), - }, - ({ item, other }: any) => eq(item.id, other.id2), - ), + { + other: q + .from({ other: rightCollection }) + .select(({ other }: any) => ({ + id2: other.id2, + name: other.name, + })), + }, + ({ item, other }: any) => eq(item.id, other.id2), + `left`, ) .select(({ item, other }: any) => ({ id: item.id, diff --git a/packages/db/tests/collection-cleanup-restart-oracle.test.ts b/packages/db/tests/collection-cleanup-restart-oracle.test.ts index 7730b9aaa7..356f91cbf0 100644 --- a/packages/db/tests/collection-cleanup-restart-oracle.test.ts +++ b/packages/db/tests/collection-cleanup-restart-oracle.test.ts @@ -5,7 +5,6 @@ import { createEffect, createLiveQueryCollection, eq, - innerJoin, withCollectionSyncConfigCleanup, } from '../src' import { createDeferred } from '../src/deferred' @@ -522,9 +521,9 @@ describe(`Collection cleanup admission oracle`, () => { q .from({ left: source }) .join( - innerJoin({ right: source }, ({ left, right }) => - eq(left.id, right.id), - ), + { right: source }, + ({ left, right }) => eq(left.id, right.id), + `inner`, ), ) const reports = vi.spyOn(console, `error`).mockImplementation(() => {}) @@ -884,9 +883,9 @@ describe(`Collection cleanup admission oracle`, () => { q .from({ first: firstSource }) .join( - innerJoin({ second: secondSource }, ({ first, second }) => - eq(first.id, second.id), - ), + { second: secondSource }, + ({ first, second }) => eq(first.id, second.id), + `inner`, ), ) const message = diff --git a/packages/db/tests/collection-query-publication-boundaries.test.ts b/packages/db/tests/collection-query-publication-boundaries.test.ts index 868c46bcbd..e07e2edbe4 100644 --- a/packages/db/tests/collection-query-publication-boundaries.test.ts +++ b/packages/db/tests/collection-query-publication-boundaries.test.ts @@ -1,6 +1,6 @@ import { expect, it } from 'vitest' import { MultiSet } from '@tanstack/db-ivm' -import { createLiveQueryCollection, eq, innerJoin } from '../src/query/index.js' +import { createLiveQueryCollection, eq } from '../src/query/index.js' import { createCollection } from '../src/collection/index.js' import { createDeferred } from '../src/deferred.js' import { createTransaction } from '../src/transactions.js' @@ -67,7 +67,7 @@ it.each([0, 2])(`opens an inner-join window from limit %s`, async (limit) => { query: (q) => q .from({ a: parent }) - .join(innerJoin({ b: child }, ({ a, b }) => eq(a.id, b.id))) + .join({ b: child }, ({ a, b }) => eq(a.id, b.id), `inner`) .orderBy(({ a }) => a.value) .limit(limit) .select(({ a }) => ({ id: a.id, value: a.value })), diff --git a/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts b/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts index 64b2ff0ee8..69c106ad18 100644 --- a/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts +++ b/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { createLiveQueryCollection, eq, leftJoin } from '../src/query/index.js' +import { createLiveQueryCollection, eq } from '../src/query/index.js' import { createCollection } from '../src/collection/index.js' import { BTreeIndex } from '../src/indexes/btree-index.js' import { mockSyncCollectionOptions } from './utils.js' @@ -505,10 +505,8 @@ describe(`CollectionSubscriber duplicate insert prevention`, () => { const liveQueryCollection = createLiveQueryCollection((q) => q .from({ users: usersCollection }) - .join( - leftJoin({ orders: ordersCollection }, ({ users: u, orders: o }) => - eq(u.id, o.userId), - ), + .join({ orders: ordersCollection }, ({ users: u, orders: o }) => + eq(u.id, o.userId), ) .select(({ users: u2, orders: o2 }) => ({ orderId: o2.id, diff --git a/packages/db/tests/conformance/suite.ts b/packages/db/tests/conformance/suite.ts index 1cb659ac88..b5d76503cc 100644 --- a/packages/db/tests/conformance/suite.ts +++ b/packages/db/tests/conformance/suite.ts @@ -40,7 +40,6 @@ import { describe, expect, it } from 'vitest' import { include, - leftJoin, pooledLiveQueries, waitForPersistedInitialRender, } from '@tanstack/db' @@ -212,11 +211,8 @@ export function runSuite(rawDriver: LiveQueryDriver) { const h = driver.mount((q) => q .from({ left: left.collection }) - .join( - leftJoin( - { right: right.collection }, - ({ left: a, right: b }: any) => ops.eq(a.id, b.id), - ), + .join({ right: right.collection }, ({ left: a, right: b }: any) => + ops.eq(a.id, b.id), ), ) await h.flush() @@ -257,10 +253,8 @@ export function runSuite(rawDriver: LiveQueryDriver) { q .from({ opted: opted.collection }) .join( - leftJoin( - { ordinary: ordinary.collection }, - ({ opted: a, ordinary: b }: any) => ops.eq(a.id, b.id), - ), + { ordinary: ordinary.collection }, + ({ opted: a, ordinary: b }: any) => ops.eq(a.id, b.id), ), ) await h.apply(settle) @@ -733,11 +727,8 @@ export function runSuite(rawDriver: LiveQueryDriver) { const h = driver.mount((q) => q .from({ issues: issues.collection }) - .join( - leftJoin( - { persons: people.collection }, - ({ issues: i, persons }: any) => ops.eq(i.userId, persons.id), - ), + .join({ persons: people.collection }, ({ issues: i, persons }: any) => + ops.eq(i.userId, persons.id), ) .select(({ issues: i, persons }: any) => ({ id: i.id, @@ -801,10 +792,8 @@ export function runSuite(rawDriver: LiveQueryDriver) { })) return q .from({ persons: people.collection }) - .join( - leftJoin({ ic: issueCounts }, ({ persons, ic }: any) => - ops.eq(persons.id, ic.userId), - ), + .leftJoin({ ic: issueCounts }, ({ persons, ic }: any) => + ops.eq(persons.id, ic.userId), ) .select(({ persons, ic }: any) => ({ name: persons.name, diff --git a/packages/db/tests/effect.test.ts b/packages/db/tests/effect.test.ts index d91708d839..60762df45a 100644 --- a/packages/db/tests/effect.test.ts +++ b/packages/db/tests/effect.test.ts @@ -1,11 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { - Query, - createEffect, - createTransaction, - eq, - leftJoin, -} from '../src/index.js' +import { Query, createEffect, createTransaction, eq } from '../src/index.js' import { createCollection } from '../src/collection/index.js' import { BTreeIndex } from '../src/indexes/btree-index.js' import { @@ -925,10 +919,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ issue: issues }) - .join( - leftJoin({ user: users }, ({ issue, user }) => - eq(issue.userId, user.id), - ), + .join({ user: users }, ({ issue, user }) => + eq(issue.userId, user.id), ) .select(({ issue, user }) => ({ issueId: issue.id, @@ -1674,12 +1666,8 @@ describe(`createEffect`, () => { return q .from({ left: leftTop }) - .join( - leftJoin( - { right: rightTop }, - ({ left: leftRow, right: rightRow }) => - eq(leftRow.id, rightRow.id), - ), + .join({ right: rightTop }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.id, rightRow.id), ) .select(({ left: leftRow, right: rightRow }) => ({ id: leftRow.id, @@ -1734,10 +1722,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join( - leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ), + .leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), ) .select(({ user, issue }) => ({ id: user.id, @@ -1779,10 +1765,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ left }) - .join( - leftJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.id, rightRow.id), - ), + .leftJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.id, rightRow.id), ), onBatch: () => {}, }) @@ -1844,10 +1828,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ left }) - .join( - leftJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.id, rightRow.id), - ), + .leftJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.id, rightRow.id), ), onBatch: () => {}, }), @@ -1957,10 +1939,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join( - leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ), + .leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), ) .select(({ user, issue }) => ({ id: user.id, @@ -2006,10 +1986,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join( - leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ), + .leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), ) .select(({ user, issue }) => ({ id: user.id, @@ -2079,10 +2057,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join( - leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ), + .leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), ) .select(({ user, issue }) => ({ id: user.id, @@ -2251,10 +2227,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join( - leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ), + .leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), ) .select(({ user, issue }) => ({ id: user.id, @@ -2310,10 +2284,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join( - leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ), + .leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), ) .select(({ user, issue }) => ({ id: user.id, @@ -2513,10 +2485,8 @@ describe(`createEffect`, () => { query: (q) => q .from({ user: users }) - .join( - leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ), + .leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), ) .fn.select(({ user }) => { if (phase === `projection`) disposeInTurn?.() diff --git a/packages/db/tests/error-sample-arguments.ts b/packages/db/tests/error-sample-arguments.ts index 16811d6b41..5201b7d695 100644 --- a/packages/db/tests/error-sample-arguments.ts +++ b/packages/db/tests/error-sample-arguments.ts @@ -91,7 +91,6 @@ export const errorSampleArguments: Record>> = { ['join clause', 'number'], ['unionAll clause', 'undefined'], ], - JoinOperatorRequiredError: [['object']], IncludeOperatorRequiredError: [['issues']], GroupingOperatorRequiredError: [[]], JoinConditionMustBeEqualityError: [[]], diff --git a/packages/db/tests/fixtures/error-codes.json b/packages/db/tests/fixtures/error-codes.json index 6fa0a66754..ab90d7ea7f 100644 --- a/packages/db/tests/fixtures/error-codes.json +++ b/packages/db/tests/fixtures/error-codes.json @@ -91,7 +91,6 @@ "GetKeyRequiredError": 178, "DuplicateTransactionIdError": 233, "SetWindowFindOneLimitError": 244, - "JoinOperatorRequiredError": 245, "GroupingOperatorRequiredError": 246, "IncludeOperatorRequiredError": 247 } diff --git a/packages/db/tests/fixtures/error-messages.json b/packages/db/tests/fixtures/error-messages.json index 3c10d78d33..b326a0211f 100644 --- a/packages/db/tests/fixtures/error-messages.json +++ b/packages/db/tests/fixtures/error-messages.json @@ -129,7 +129,7 @@ ], "InvalidSourceTypeError": [ "Invalid source for from clause: Expected an object with a single key-value pair like { alias: collection }. For example: .from({ todos: todosCollection }). Got: string", - "Invalid source for join clause: Expected an object with a single key-value pair like { alias: collection }. For example: .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Got: number", + "Invalid source for join clause: Expected an object with a single key-value pair like { alias: collection }. For example: .join({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id)). Got: number", "Invalid source for unionAll clause: Expected an object with one or more key-value pairs like { alias: collection }. For example: .unionAll({ todos: todosCollection, events: eventsCollection }). Got: undefined" ], "JoinConditionMustBeEqualityError": [ @@ -261,9 +261,6 @@ "SetWindowFindOneLimitError": [ "setWindow() cannot change the limit of a findOne() query." ], - "JoinOperatorRequiredError": [ - ".join() takes a join operator, such as .join(leftJoin({ todos: todosCollection }, ({ todo, todos }) => eq(todo.id, todos.id))). Import leftJoin, innerJoin, rightJoin, fullJoin, or joinOf from @tanstack/db. Got: object" - ], "GroupingOperatorRequiredError": [ "groupBy() without an aggregate needs groupKeys(), such as .groupBy(groupKeys(({ todo }) => todo.status)). Import groupKeys from @tanstack/db. A query with an aggregate such as count() needs nothing extra." ], diff --git a/packages/db/tests/fixtures/loose-typescript-group-keys.ts b/packages/db/tests/fixtures/loose-typescript-group-keys.ts new file mode 100644 index 0000000000..d42134357b --- /dev/null +++ b/packages/db/tests/fixtures/loose-typescript-group-keys.ts @@ -0,0 +1,23 @@ +// Compiled by group-keys-typescript-modes.test.ts with noImplicitAny on and off. +import { createLiveQueryCollection, groupKeys } from '../../src/index.js' +import type { Collection, ContextFromSource } from '../../src/index.js' + +type User = { id: number; name: string } +declare const users: Collection + +export const grouped = createLiveQueryCollection((q) => + q + .from({ user: users }) + .groupBy(groupKeys(({ user }) => user.name)) + .select(({ user }) => ({ name: user.name })), +) + +// Group keys built outside `.groupBy()` pass the context explicitly. +type UserQuery = ContextFromSource<{ user: typeof users }> +export const nameKeys = groupKeys(({ user }) => user.name) +export const regrouped = createLiveQueryCollection((q) => + q + .from({ user: users }) + .groupBy(nameKeys) + .select(({ user }) => ({ name: user.name })), +) diff --git a/packages/db/tests/fixtures/loose-typescript-operators.ts b/packages/db/tests/fixtures/loose-typescript-operators.ts deleted file mode 100644 index 6f2e5e2f57..0000000000 --- a/packages/db/tests/fixtures/loose-typescript-operators.ts +++ /dev/null @@ -1,75 +0,0 @@ -// Compiled by operator-typescript-modes.test.ts with noImplicitAny on and off. -import { - createLiveQueryCollection, - eq, - groupKeys, - innerJoin, - joinOf, - leftJoin, -} from '../../src/index.js' -import type { - Collection, - ContextFromSource, - JoinCondition, -} from '../../src/index.js' - -type User = { id: number; name: string } -type Post = { id: number; userId: number; title: string } -declare const users: Collection -declare const posts: Collection -declare const type: `left` | `inner` - -export const joined = createLiveQueryCollection((q) => - q - .from({ user: users }) - .join( - leftJoin({ post: posts }, ({ user, post }) => eq(user.id, post.userId)), - ) - .join( - innerJoin({ again: posts }, ({ user, again }) => - eq(user.id, again.userId), - ), - ) - .join( - joinOf(type, { third: posts }, ({ user, third }) => - eq(user.id, third.userId), - ), - ) - .select(({ user, post }) => ({ name: user.name, title: post.title })), -) - -export const grouped = createLiveQueryCollection((q) => - q - .from({ user: users }) - .groupBy(groupKeys(({ user }) => user.name)) - .select(({ user }) => ({ name: user.name })), -) - -// A reusable operator built outside `.join()` passes its context explicitly. -type UserQuery = ContextFromSource<{ user: typeof users }> -export const withPosts = leftJoin<{ post: typeof posts }, UserQuery>( - { post: posts }, - ({ user, post }) => eq(user.id, post.userId), -) -export const reused = createLiveQueryCollection((q) => - q - .from({ user: users }) - .join(withPosts) - .select(({ user, post }) => ({ name: user.name, title: post.title })), -) - -export const statusKeys = groupKeys(({ user }) => user.name) -export const regrouped = createLiveQueryCollection((q) => - q - .from({ user: users }) - .groupBy(statusKeys) - .select(({ user }) => ({ name: user.name })), -) - -// A factory takes a condition of the exported type. -export const withPostsOn = ( - on: JoinCondition, -) => leftJoin<{ post: typeof posts }, UserQuery>({ post: posts }, on) -export const factoryJoin = withPostsOn(({ user, post }) => - eq(user.id, post.userId), -) diff --git a/packages/db/tests/group-keys-typescript-modes.test.ts b/packages/db/tests/group-keys-typescript-modes.test.ts new file mode 100644 index 0000000000..698d0bab0f --- /dev/null +++ b/packages/db/tests/group-keys-typescript-modes.test.ts @@ -0,0 +1,110 @@ +// @vitest-environment node +import { fileURLToPath } from 'node:url' +import ts from 'typescript' +import { describe, expect, it } from 'vitest' + +// `groupKeys()` gets the query's context from `.groupBy()`, by inference. +// TypeScript skips that inference without noImplicitAny, so its callback must +// still compile there, untyped like other callbacks in that mode. Strict mode +// must keep it typed. Group keys that pass their context explicitly are typed +// in both modes. Type tests run in strict mode only, so this compiles the +// fixtures in both modes. +const fixture = (name: string) => + fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)) +const config = ts.getParsedCommandLineOfConfigFile( + fileURLToPath(new URL(`../tsconfig.json`, import.meta.url)), + {}, + { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, +)! + +// Sources with deliberate errors live in memory, so the project's own +// typecheck does not see them. +const header = `import { createLiveQueryCollection, groupKeys } from '../../src/index.js' +import type { Collection, ContextFromSource } from '../../src/index.js' + +type User = { id: number; name: string } +declare const users: Collection +type UserQuery = ContextFromSource<{ user: typeof users }> +` +type Form = `inline` | `reusable with context` | `reusable without context` + +// A wrong field in each form. Reusable group keys are built outside +// `.groupBy()`, so only an explicit context can type them. +const wrongField: Record = { + inline: `export const q1 = createLiveQueryCollection((q) => + q.from({ user: users }).groupBy(groupKeys(({ user }) => user.nope)), +)`, + 'reusable with context': `export const keys = groupKeys(({ user }) => user.nope)`, + 'reusable without context': `export const keys = groupKeys(({ user }) => user.nope)`, +} + +// Whether each form must report the wrong field, in strict mode and without +// noImplicitAny. Inline operators follow the mode, as other callbacks do. An +// explicit context types a reusable operator in both modes. Without one, +// TypeScript has no context to infer, and the operator is untyped in both: +// a documented limit, which the guide answers with the explicit form. +const reportsWrongField: Record = { + inline: { strict: true, loose: false }, + 'reusable with context': { strict: true, loose: true }, + 'reusable without context': { strict: false, loose: false }, +} + +const forms = Object.keys(wrongField) as Array +const fileOf = (form: Form) => + fixture(`wrong-field-group-keys-${form.replaceAll(` `, `-`)}.ts`) +const inMemory = new Map( + forms.map((form) => [fileOf(form), `${header}${wrongField[form]}\n`]), +) + +// One program per mode compiles every file; each test reads its own file's +// diagnostics. A program per file would take seconds per test. +const programs = new Map() +function errors(file: string, noImplicitAny: boolean) { + let program = programs.get(noImplicitAny) + if (!program) { + const options = { + ...config.options, + noImplicitAny, + noUnusedLocals: false, + noEmit: true, + } + const host = ts.createCompilerHost(options) + const readFile = host.readFile + host.readFile = (path) => inMemory.get(path) ?? readFile(path) + const fileExists = host.fileExists + host.fileExists = (path) => inMemory.has(path) || fileExists(path) + program = ts.createProgram( + [fixture(`loose-typescript-group-keys.ts`), ...inMemory.keys()], + options, + host, + ) + programs.set(noImplicitAny, program) + } + return ts + .getPreEmitDiagnostics(program, program.getSourceFile(file)) + .map((d) => ts.flattenDiagnosticMessageText(d.messageText, ` `)) +} + +describe(`groupKeys callbacks across TypeScript modes`, () => { + it.each([true, false])( + `compile with noImplicitAny %s`, + (noImplicitAny) => { + expect( + errors(fixture(`loose-typescript-group-keys.ts`), noImplicitAny), + ).toEqual([]) + // Building a program takes seconds; later tests reuse it. + }, + 30_000, + ) + + it.each(forms)( + `reports a wrong field per mode: %s`, + (form) => { + const expected = reportsWrongField[form] + const wrong = [expect.stringContaining(`Property 'nope' does not exist`)] + expect(errors(fileOf(form), true)).toEqual(expected.strict ? wrong : []) + expect(errors(fileOf(form), false)).toEqual(expected.loose ? wrong : []) + }, + 30_000, + ) +}) diff --git a/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts b/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts index 2be228ec11..6ba058ed65 100644 --- a/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts +++ b/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts @@ -55,7 +55,6 @@ import { createLiveQueryObserver, eq, getLiveQueryHash, - leftJoin, prepareLiveQueryValue, } from '../src' import { Query } from '../src/query/builder/index.js' @@ -1223,7 +1222,7 @@ describe(`a source whose start throws on resumption`, () => { query: (q) => q .from({ a: failing }) - .join(leftJoin({ b: healthy }, ({ a, b }) => eq(a.id, b.id))), + .join({ b: healthy }, ({ a, b }) => eq(a.id, b.id)), gcTime: 0, }) const consoleError = vi.spyOn(console, `error`).mockImplementation(() => {}) diff --git a/packages/db/tests/live-query-options.test.ts b/packages/db/tests/live-query-options.test.ts index 3482b23759..4dc2e54d8c 100644 --- a/packages/db/tests/live-query-options.test.ts +++ b/packages/db/tests/live-query-options.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { leftJoin } from '@tanstack/db' import { include, pooledLiveQueries } from '../src/query/index.js' import { createCollection } from '../src/collection/index.js' import { DbClient, collectionOptions } from '../src/client.js' @@ -124,9 +123,8 @@ describe(`live query preparation`, () => { renderDeferrals.clear() const continued = prepared.join( - leftJoin({ later: laterDescriptor }, ({ source, later }) => - eq(source.id, later.id), - ), + { later: laterDescriptor }, + ({ source, later }) => eq(source.id, later.id), ) expect( collectSourceRefs(getQueryIR(continued)).map((ref) => ref.type), @@ -169,9 +167,8 @@ describe(`live query preparation`, () => { deferrals, ) as typeof base const continued = prepared.join( - leftJoin({ later: laterDescriptor }, ({ source, later }) => - eq(source.id, later.id), - ), + { later: laterDescriptor }, + ({ source, later }) => eq(source.id, later.id), ) expect(starts).toBe(0) @@ -216,9 +213,8 @@ describe(`live query preparation`, () => { ) const continued = captured!.join( - leftJoin({ later: laterDescriptor }, ({ source, later }) => - eq(source.id, later.id), - ), + { later: laterDescriptor }, + ({ source, later }) => eq(source.id, later.id), ) expect( collectSourceRefs(getQueryIR(continued)).map((ref) => ref.type), diff --git a/packages/db/tests/operator-typescript-modes.test.ts b/packages/db/tests/operator-typescript-modes.test.ts deleted file mode 100644 index 4cb933cf24..0000000000 --- a/packages/db/tests/operator-typescript-modes.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -// @vitest-environment node -import { fileURLToPath } from 'node:url' -import ts from 'typescript' -import { describe, expect, it } from 'vitest' - -// An operator value, such as a join operator or `groupKeys()`, gets the -// query's context from the clause it is passed to, by inference. TypeScript -// skips that inference without noImplicitAny, so the operator's callback must -// still compile there, untyped like other callbacks in that mode. Strict mode -// must keep it typed. A reusable operator that passes its context explicitly -// is typed in both modes. Type tests run in strict mode only, so this compiles -// the fixtures in both modes. -const fixture = (name: string) => - fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)) -const config = ts.getParsedCommandLineOfConfigFile( - fileURLToPath(new URL(`../tsconfig.json`, import.meta.url)), - {}, - { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, -)! - -// Sources with deliberate errors live in memory, so the project's own -// typecheck does not see them. -const header = `import { createLiveQueryCollection, eq, groupKeys, leftJoin } from '../../src/index.js' -import type { Collection, ContextFromSource } from '../../src/index.js' - -type User = { id: number; name: string } -type Post = { id: number; userId: number } -declare const users: Collection -declare const posts: Collection -type UserQuery = ContextFromSource<{ user: typeof users }> -` -type Form = `inline` | `reusable with context` | `reusable without context` -type Operator = `join` | `groupKeys` - -// A wrong field in each operator and form. A reusable operator is built -// outside its clause, so only an explicit context can type it. -const wrongField: Record> = { - join: { - inline: `export const q1 = createLiveQueryCollection((q) => - q.from({ user: users }).join(leftJoin({ post: posts }, ({ user, post }) => eq(user.nope, post.userId))), -)`, - 'reusable with context': `export const op = leftJoin<{ post: typeof posts }, UserQuery>( - { post: posts }, - ({ user, post }) => eq(user.nope, post.userId), -)`, - 'reusable without context': `export const op = leftJoin({ post: posts }, ({ user, post }) => eq(user.nope, post.userId))`, - }, - groupKeys: { - inline: `export const q1 = createLiveQueryCollection((q) => - q.from({ user: users }).groupBy(groupKeys(({ user }) => user.nope)), -)`, - 'reusable with context': `export const keys = groupKeys(({ user }) => user.nope)`, - 'reusable without context': `export const keys = groupKeys(({ user }) => user.nope)`, - }, -} - -// Whether each form must report the wrong field, in strict mode and without -// noImplicitAny. Inline operators follow the mode, as other callbacks do. An -// explicit context types a reusable operator in both modes. Without one, -// TypeScript has no context to infer, and the operator is untyped in both: -// a documented limit, which the guide answers with the explicit form. -const reportsWrongField: Record = { - inline: { strict: true, loose: false }, - 'reusable with context': { strict: true, loose: true }, - 'reusable without context': { strict: false, loose: false }, -} - -const cases = (Object.keys(wrongField) as Array).flatMap((operator) => - (Object.keys(reportsWrongField) as Array).map( - (form) => [operator, form] as const, - ), -) -const fileOf = (operator: Operator, form: Form) => - fixture(`wrong-field-${operator}-${form.replaceAll(` `, `-`)}.ts`) -const inMemory = new Map( - cases.map(([operator, form]) => [ - fileOf(operator, form), - `${header}${wrongField[operator][form]}\n`, - ]), -) - -// One program per mode compiles every file; each test reads its own file's -// diagnostics. A program per file would take seconds per test. -const programs = new Map() -function errors(file: string, noImplicitAny: boolean) { - let program = programs.get(noImplicitAny) - if (!program) { - const options = { - ...config.options, - noImplicitAny, - noUnusedLocals: false, - noEmit: true, - } - const host = ts.createCompilerHost(options) - const readFile = host.readFile - host.readFile = (path) => inMemory.get(path) ?? readFile(path) - const fileExists = host.fileExists - host.fileExists = (path) => inMemory.has(path) || fileExists(path) - program = ts.createProgram( - [fixture(`loose-typescript-operators.ts`), ...inMemory.keys()], - options, - host, - ) - programs.set(noImplicitAny, program) - } - return ts - .getPreEmitDiagnostics(program, program.getSourceFile(file)) - .map((d) => ts.flattenDiagnosticMessageText(d.messageText, ` `)) -} - -describe(`operator callbacks across TypeScript modes`, () => { - it.each([true, false])(`compile with noImplicitAny %s`, (noImplicitAny) => { - expect( - errors(fixture(`loose-typescript-operators.ts`), noImplicitAny), - ).toEqual([]) - }) - - it.each(cases)(`reports a wrong field per mode: %s, %s`, (operator, form) => { - const file = fileOf(operator, form) - const expected = reportsWrongField[form] - const wrong = [expect.stringContaining(`Property 'nope' does not exist`)] - expect(errors(file, true)).toEqual(expected.strict ? wrong : []) - expect(errors(file, false)).toEqual(expected.loose ? wrong : []) - }) -}) diff --git a/packages/db/tests/opt-in-operator-bundle.test.ts b/packages/db/tests/opt-in-operator-bundle.test.ts index 4804d974d4..cf7d00509a 100644 --- a/packages/db/tests/opt-in-operator-bundle.test.ts +++ b/packages/db/tests/opt-in-operator-bundle.test.ts @@ -6,10 +6,10 @@ import { describe, expect, it } from 'vitest' /** * # Does an app pay only for the operators it imports? * - * Joins, grouping, includes, and pooling are opt-in. Each one's code arrives - * with the value that names it: a join operator (`leftJoin`), an aggregate - * (`count`) or `groupKeys`, an include function (`include`, `toArray`, - * `materialize`), and `pooledLiveQueries`. The law: an app's production + * Grouping, includes, and pooling are opt-in. Each one's code arrives with + * the value that names it: an aggregate (`count`) or `groupKeys`, an include + * function (`include`, `toArray`, `materialize`), and `pooledLiveQueries`. + * Joins stay in core, so every live-query app bundles them. The law: an app's production * bundle contains an operator's modules exactly when the app imports one of * those values. A static import from shared code would break it silently, so * this bundles small apps from source as an app's build would and reads which @@ -19,17 +19,11 @@ import { describe, expect, it } from 'vitest' * for its own modules: without them, a module renamed or moved would make the * absence checks pass vacuously. It also runs every combination of operator * families, each family named by one of its values in turn, so a module that - * only a combination reaches is checked too. The modules shared by joins and - * includes must arrive exactly when an app imports one of the two. + * only a combination reaches is checked too. */ const index = fileURLToPath(new URL(`../src/index.ts`, import.meta.url)) const modules = { - joins: [ - `src/query/compiler/joins.ts`, - `src/query/compiler/join-runtime.ts`, - `src/query/optimizer-joins.ts`, - ], grouping: [ `src/query/compiler/grouping.ts`, `src/query/compiler/group-by.ts`, @@ -45,15 +39,17 @@ type Family = keyof typeof modules /** The values that bring each family's code in. */ const values: Record> = { - joins: [`leftJoin`, `innerJoin`, `rightJoin`, `fullJoin`, `joinOf`], grouping: [`count`, `sum`, `avg`, `min`, `max`, `groupKeys`], includes: [`include`, `toArray`, `materialize`], pooling: [`pooledLiveQueries`], } -// Joins and includes share public-key reduction; an app with neither keeps it -// out. -const shared = [`src/query/live/public-key-reduction.ts`] +// Joins are core: their modules, and the public-key reduction that joins and +// includes share, are in every live-query app. +const core = [ + `src/query/compiler/joins.ts`, + `src/query/live/public-key-reduction.ts`, +] const families = Object.keys(modules) as Array /** Bundles an app that exports `names` and returns its modules' output bytes. */ @@ -102,10 +98,7 @@ function expectOperators( for (const family of families) for (const file of modules[family]) expect(bytes.has(file), `${family}: ${file}`).toBe(imported.has(family)) - for (const file of shared) - expect(bytes.has(file), file).toBe( - imported.has(`joins`) || imported.has(`includes`), - ) + for (const file of core) expect(bytes.has(file), file).toBe(true) } describe(`opt-in operator bundles`, () => { diff --git a/packages/db/tests/query/aggregate-value-contracts.test-d.ts b/packages/db/tests/query/aggregate-value-contracts.test-d.ts index 4114706306..687409e8b4 100644 --- a/packages/db/tests/query/aggregate-value-contracts.test-d.ts +++ b/packages/db/tests/query/aggregate-value-contracts.test-d.ts @@ -1,5 +1,5 @@ import { describe, expectTypeOf, test } from 'vitest' -import { createLiveQueryCollection, leftJoin } from '../../src/query/index.js' +import { createLiveQueryCollection } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { add, @@ -244,10 +244,8 @@ describe(`aggregate value contracts`, () => { query: (q) => q .from({ row: rows }) - .join( - leftJoin({ detail: details }, ({ row, detail }) => - eq(row.id, detail.rowId), - ), + .leftJoin({ detail: details }, ({ row, detail }) => + eq(row.id, detail.rowId), ) .groupBy(({ row }) => row.group) .select(({ row, detail }) => { diff --git a/packages/db/tests/query/async-hold-outcomes.test.ts b/packages/db/tests/query/async-hold-outcomes.test.ts index 38e16a10f2..3590483138 100644 --- a/packages/db/tests/query/async-hold-outcomes.test.ts +++ b/packages/db/tests/query/async-hold-outcomes.test.ts @@ -1,9 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest' -import { - createLiveQueryCollection, - eq, - leftJoin, -} from '../../src/query/index.js' +import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { createEffect } from '../../src/query/effect.js' @@ -164,10 +160,8 @@ describe(`live-query lazy demand hold outcomes`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ user: users }) - .join( - leftJoin({ post: posts.collection }, ({ user, post }) => - eq(user.id, post.userId), - ), + .leftJoin({ post: posts.collection }, ({ user, post }) => + eq(user.id, post.userId), ) .select(({ user, post }) => ({ id: user.id, postId: post.id })), ) @@ -196,10 +190,8 @@ describe(`live-query lazy demand hold outcomes`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ user: users }) - .join( - leftJoin({ post: posts.collection }, ({ user, post }) => - eq(user.id, post.userId), - ), + .leftJoin({ post: posts.collection }, ({ user, post }) => + eq(user.id, post.userId), ) .select(({ user, post }) => ({ id: user.id, postId: post.id })), ) diff --git a/packages/db/tests/query/builder/buildQuery.test.ts b/packages/db/tests/query/builder/buildQuery.test.ts index 983296bc8d..292d6f892b 100644 --- a/packages/db/tests/query/builder/buildQuery.test.ts +++ b/packages/db/tests/query/builder/buildQuery.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { leftJoin } from '@tanstack/db' import { CollectionImpl } from '../../../src/collection/index.js' import { buildQuery } from '../../../src/query/builder/index.js' import { and, eq, gt, or } from '../../../src/query/builder/functions.js' @@ -62,11 +61,9 @@ describe(`buildQuery function`, () => { q .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .select(({ employees, departments }) => ({ employee_name: employees.name, @@ -124,10 +121,8 @@ describe(`buildQuery function`, () => { const query = buildQuery((q) => q .from({ comment: commentsCollection }) - .join( - leftJoin({ user: usersCollection }, ({ comment, user }) => - eq(comment.user_id, user.id), - ), + .join({ user: usersCollection }, ({ comment, user }) => + eq(comment.user_id, user.id), ) .where(({ comment }) => or(eq(comment.id, 1), eq(comment.id, 2))) .orderBy(({ comment }) => comment.date, `desc`) diff --git a/packages/db/tests/query/builder/callback-types.test-d.ts b/packages/db/tests/query/builder/callback-types.test-d.ts index 3fe061c8a5..a26972b290 100644 --- a/packages/db/tests/query/builder/callback-types.test-d.ts +++ b/packages/db/tests/query/builder/callback-types.test-d.ts @@ -1,5 +1,4 @@ import { describe, expectTypeOf, test } from 'vitest' -import { leftJoin } from '@tanstack/db' import { createCollection } from '../../../src/collection/index.js' import { mockSyncCollectionOptions } from '../../utils.js' import { Query } from '../../../src/query/builder/index.js' @@ -115,10 +114,8 @@ describe(`Query Builder Callback Types`, () => { test(`refProxy with joins in select callback`, () => { new Query() .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => { // Test cross-table property access @@ -271,10 +268,8 @@ describe(`Query Builder Callback Types`, () => { test(`refProxy with joins in where callback`, () => { new Query() .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .where(({ user, dept }) => { expectTypeOf(user.active).toEqualTypeOf>() @@ -292,8 +287,9 @@ describe(`Query Builder Callback Types`, () => { describe(`JOIN callback types`, () => { test(`refProxy types in join on callback`, () => { - new Query().from({ user: usersCollection }).join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { + new Query() + .from({ user: usersCollection }) + .join({ dept: departmentsCollection }, ({ user, dept }) => { // Test property access for join conditions expectTypeOf( user.department_id, @@ -301,49 +297,39 @@ describe(`Query Builder Callback Types`, () => { expectTypeOf(dept.id).toEqualTypeOf>() return eq(user.department_id, dept.id) - }), - ) + }) }) test(`complex join conditions`, () => { - new Query().from({ user: usersCollection }).join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { + new Query() + .from({ user: usersCollection }) + .join({ dept: departmentsCollection }, ({ user, dept }) => { // Test complex join conditions with multiple operators expectTypeOf( and(eq(user.department_id, dept.id), eq(dept.active, true)), ).toEqualTypeOf>() return and(eq(user.department_id, dept.id), eq(dept.active, true)) - }), - ) + }) }) test(`multiple joins with correct context`, () => { new Query() .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), - ) - .join( - leftJoin( - { project: projectsCollection }, - ({ user, dept, project }) => { - expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept.id).toEqualTypeOf>() - expectTypeOf(project.user_id).toEqualTypeOf>() - expectTypeOf(project.department_id).toEqualTypeOf< - RefLeaf - >() - - return and( - eq(project.user_id, user.id), - eq(project.department_id, dept.id), - ) - }, - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) + .join({ project: projectsCollection }, ({ user, dept, project }) => { + expectTypeOf(user.id).toEqualTypeOf>() + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(project.user_id).toEqualTypeOf>() + expectTypeOf(project.department_id).toEqualTypeOf>() + + return and( + eq(project.user_id, user.id), + eq(project.department_id, dept.id), + ) + }) }) }) @@ -375,10 +361,8 @@ describe(`Query Builder Callback Types`, () => { test(`orderBy with joins`, () => { new Query() .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .orderBy(({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() @@ -417,10 +401,8 @@ describe(`Query Builder Callback Types`, () => { test(`groupBy with joins`, () => { new Query() .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .groupBy(({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() @@ -500,10 +482,8 @@ describe(`Query Builder Callback Types`, () => { test(`having with joins`, () => { new Query() .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .groupBy(({ dept }) => dept.location) .having(({ user, dept }) => { @@ -520,29 +500,20 @@ describe(`Query Builder Callback Types`, () => { test(`complex query with all callback types`, () => { new Query() .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => { - expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept.id).toEqualTypeOf>() - expectTypeOf(dept.location).toEqualTypeOf>() - return eq(user.department_id, dept.id) - }), - ) - .join( - leftJoin( - { project: projectsCollection }, - ({ user, dept, project }) => { - expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept.id).toEqualTypeOf>() - expectTypeOf(dept.location).toEqualTypeOf>() - expectTypeOf(project.user_id).toEqualTypeOf>() - expectTypeOf(project.department_id).toEqualTypeOf< - RefLeaf - >() - return eq(project.user_id, user.id) - }, - ), - ) + .join({ dept: departmentsCollection }, ({ user, dept }) => { + expectTypeOf(user.id).toEqualTypeOf>() + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(dept.location).toEqualTypeOf>() + return eq(user.department_id, dept.id) + }) + .join({ project: projectsCollection }, ({ user, dept, project }) => { + expectTypeOf(user.id).toEqualTypeOf>() + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(dept.location).toEqualTypeOf>() + expectTypeOf(project.user_id).toEqualTypeOf>() + expectTypeOf(project.department_id).toEqualTypeOf>() + return eq(project.user_id, user.id) + }) .where(({ user, dept, project }) => { expectTypeOf(user.id).toEqualTypeOf>() expectTypeOf(dept.id).toEqualTypeOf>() diff --git a/packages/db/tests/query/builder/from.test.ts b/packages/db/tests/query/builder/from.test.ts index 362e9205ad..17270cdb0e 100644 --- a/packages/db/tests/query/builder/from.test.ts +++ b/packages/db/tests/query/builder/from.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { leftJoin } from '@tanstack/db' import { CollectionImpl } from '../../../src/collection/index.js' import { Query, getQueryIR } from '../../../src/query/builder/index.js' import { eq } from '../../../src/query/builder/functions.js' @@ -158,7 +157,7 @@ describe(`QueryBuilder.from`, () => { const builder = new Query().from({ employees: employeesCollection }) expect(() => { - builder.join(leftJoin(`departments` as any, () => eq(1, 1))) + builder.join(`departments` as any, () => eq(1, 1)) }).toThrow( /Invalid source for join clause: Expected an object with a single key-value pair.*For example: \.join/, ) diff --git a/packages/db/tests/query/builder/functional-variants.test.ts b/packages/db/tests/query/builder/functional-variants.test.ts index 346c37c621..b993d77b9d 100644 --- a/packages/db/tests/query/builder/functional-variants.test.ts +++ b/packages/db/tests/query/builder/functional-variants.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { leftJoin } from '@tanstack/db' import { CollectionImpl } from '../../../src/collection/index.js' import { Query, getQueryIR } from '../../../src/query/builder/index.js' import { eq, gt } from '../../../src/query/builder/functions.js' @@ -74,11 +73,9 @@ describe(`QueryBuilder functional variants (fn)`, () => { const query = new Query() .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .fn.select((row) => ({ employeeName: row.employees.name, @@ -146,11 +143,9 @@ describe(`QueryBuilder functional variants (fn)`, () => { const query = new Query() .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .fn.where( (row) => @@ -222,11 +217,9 @@ describe(`QueryBuilder functional variants (fn)`, () => { const query = new Query() .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .groupBy(({ departments }) => departments.name) .fn.having( @@ -246,11 +239,9 @@ describe(`QueryBuilder functional variants (fn)`, () => { const query = new Query() .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .fn.where((row) => row.employees.active) .fn.where((row) => row.employees.salary > 40000) diff --git a/packages/db/tests/query/builder/join.test.ts b/packages/db/tests/query/builder/join.test.ts index 464dcdd508..f76d8ad443 100644 --- a/packages/db/tests/query/builder/join.test.ts +++ b/packages/db/tests/query/builder/join.test.ts @@ -1,9 +1,7 @@ import { describe, expect, it } from 'vitest' -import { fullJoin, innerJoin, leftJoin, rightJoin } from '@tanstack/db' import { CollectionImpl } from '../../../src/collection/index.js' import { Query, getQueryIR } from '../../../src/query/builder/index.js' import { and, eq, gt } from '../../../src/query/builder/functions.js' -import { JoinOperatorRequiredError } from '../../../src/errors.js' // Test schema interface Employee { @@ -39,11 +37,9 @@ describe(`QueryBuilder.join`, () => { const query = builder .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) const builtQuery = getQueryIR(query) @@ -74,18 +70,12 @@ describe(`QueryBuilder.join`, () => { const query = builder .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) - .join( - leftJoin( - { projects: projectsCollection }, - ({ departments, projects }) => - eq(departments.id, projects.department_id), - ), + .join({ projects: projectsCollection }, ({ departments, projects }) => + eq(departments.id, projects.department_id), ) const builtQuery = getQueryIR(query) @@ -104,11 +94,9 @@ describe(`QueryBuilder.join`, () => { const query = builder .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .select(({ employees, departments }) => ({ id: employees.id, @@ -130,11 +118,9 @@ describe(`QueryBuilder.join`, () => { const query = builder .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .where(({ departments }) => gt(departments.budget, 1000000)) @@ -151,10 +137,8 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join( - leftJoin({ bigDepts: subQuery as any }, ({ employees, bigDepts }) => - eq(employees.department_id, (bigDepts as any).id), - ), + .join({ bigDepts: subQuery as any }, ({ employees, bigDepts }) => + eq(employees.department_id, (bigDepts as any).id), ) const builtQuery = getQueryIR(query) @@ -171,11 +155,9 @@ describe(`QueryBuilder.join`, () => { const query = builder .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .where(({ employees, departments }) => and(gt(employees.salary, 50000), gt(departments.budget, 1000000)), @@ -212,16 +194,12 @@ describe(`QueryBuilder.join`, () => { const query = builder .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) - .join( - leftJoin({ users: usersCollection }, ({ employees, users }) => - eq(employees.id, users.employee_id), - ), + .join({ users: usersCollection }, ({ employees, users }) => + eq(employees.id, users.employee_id), ) const builtQuery = getQueryIR(query) @@ -240,11 +218,9 @@ describe(`QueryBuilder.join`, () => { const query = builder .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .select(({ employees, departments }) => ({ employee: employees, @@ -263,21 +239,18 @@ describe(`QueryBuilder.join`, () => { const explicitQuery = builder .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + `left`, ) const aliasQuery = builder .from({ employees: employeesCollection }) - .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + .leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) const explicitQueryIR = getQueryIR(explicitQuery) @@ -292,21 +265,18 @@ describe(`QueryBuilder.join`, () => { const explicitQuery = builder .from({ employees: employeesCollection }) .join( - rightJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + `right`, ) const aliasQuery = builder .from({ employees: employeesCollection }) - .join( - rightJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + .rightJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) const explicitQueryIR = getQueryIR(explicitQuery) @@ -321,21 +291,18 @@ describe(`QueryBuilder.join`, () => { const explicitQuery = builder .from({ employees: employeesCollection }) .join( - innerJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + `inner`, ) const aliasQuery = builder .from({ employees: employeesCollection }) - .join( - innerJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + .innerJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) const explicitQueryIR = getQueryIR(explicitQuery) @@ -350,21 +317,18 @@ describe(`QueryBuilder.join`, () => { const explicitQuery = builder .from({ employees: employeesCollection }) .join( - fullJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + `full`, ) const aliasQuery = builder .from({ employees: employeesCollection }) - .join( - fullJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + .fullJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) const explicitQueryIR = getQueryIR(explicitQuery) @@ -388,19 +352,15 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + .leftJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) - .join( - innerJoin( - { projects: projectsCollection }, - ({ departments, projects }) => - eq(departments.id, projects.department_id), - ), + .innerJoin( + { projects: projectsCollection }, + ({ departments, projects }) => + eq(departments.id, projects.department_id), ) const builtQuery = getQueryIR(query) @@ -420,12 +380,10 @@ describe(`QueryBuilder.join`, () => { const builder = new Query() const query = builder .from({ employees: employeesCollection }) - .join( - innerJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + .innerJoin( + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .where(({ departments }) => gt(departments.budget, 1000000)) .select(({ employees, departments }) => ({ @@ -444,13 +402,3 @@ describe(`QueryBuilder.join`, () => { }) }) }) - -describe(`join operator values`, () => { - it(`rejects the removed source-and-callback call shape with a coded error`, () => { - const builder = new Query().from({ employees: employeesCollection }) as any - expect(() => - builder.join({ departments: departmentsCollection }, () => true), - ).toThrow(JoinOperatorRequiredError) - expect(() => builder.join(undefined)).toThrow(/Got: undefined/) - }) -}) diff --git a/packages/db/tests/query/builder/subqueries.test-d.ts b/packages/db/tests/query/builder/subqueries.test-d.ts index 14690f59a1..3b9358d0a5 100644 --- a/packages/db/tests/query/builder/subqueries.test-d.ts +++ b/packages/db/tests/query/builder/subqueries.test-d.ts @@ -1,5 +1,4 @@ import { describe, expectTypeOf, test } from 'vitest' -import { leftJoin } from '@tanstack/db' import { Query } from '../../../src/query/builder/index.js' import { CollectionImpl } from '../../../src/collection/index.js' import { avg, count, eq } from '../../../src/query/builder/functions.js' @@ -119,10 +118,8 @@ describe(`Subquery Types`, () => { // This should work WITHOUT any cast const _query = new Query() .from({ issue: issuesCollection }) - .join( - leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ), + .join({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), ) .select(({ issue, activeUser }) => ({ issueId: issue.id, @@ -153,10 +150,8 @@ describe(`Subquery Types`, () => { // This should work WITHOUT any cast const _query = new Query() .from({ issue: issuesCollection }) - .join( - leftJoin({ activeUser: userNamesQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ), + .join({ activeUser: userNamesQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), ) .select(({ issue, activeUser }) => ({ issueId: issue.id, @@ -265,10 +260,8 @@ describe(`Subquery Types`, () => { // Join regular collection with subquery - NO CAST! const _query = new Query() .from({ issue: issuesCollection }) - .join( - leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ), + .join({ activeUser: activeUsers }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), ) .select(({ issue, activeUser }) => ({ issueId: issue.id, @@ -293,10 +286,8 @@ describe(`Subquery Types`, () => { // Join subquery with regular collection - NO CAST! const _query = new Query() .from({ issue: filteredIssues }) - .join( - leftJoin({ user: usersCollection }, ({ issue, user }) => - eq(issue.userId, user.id), - ), + .join({ user: usersCollection }, ({ issue, user }) => + eq(issue.userId, user.id), ) .select(({ issue, user }) => ({ issueId: issue.id, diff --git a/packages/db/tests/query/builder/union-all.test.ts b/packages/db/tests/query/builder/union-all.test.ts index ada09739c7..ed08ab0a1a 100644 --- a/packages/db/tests/query/builder/union-all.test.ts +++ b/packages/db/tests/query/builder/union-all.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { innerJoin } from '@tanstack/db' import { CollectionImpl } from '../../../src/collection/index.js' import { DbClient, collectionOptions } from '../../../src/client.js' import { @@ -119,11 +118,10 @@ describe(`QueryBuilder.unionAll`, () => { const query = builder .unionAll({ employees: employeeDescriptor }) .join( - innerJoin( - { departments: departmentDescriptor }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentDescriptor }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), + `inner`, ) expect(collectSourceRefs(getQueryIR(query)).map((ref) => ref.type)).toEqual( @@ -162,10 +160,9 @@ describe(`QueryBuilder.unionAll`, () => { const query = builder .unionAll(employeeRows, departmentRows) .join( - innerJoin( - { departments: departmentDescriptor }, - ({ id, departments }) => eq(id, departments.id), - ), + { departments: departmentDescriptor }, + ({ id, departments }) => eq(id, departments.id), + `inner`, ) expect(collectSourceRefs(getQueryIR(query)).map((ref) => ref.type)).toEqual( diff --git a/packages/db/tests/query/case-when.test-d.ts b/packages/db/tests/query/case-when.test-d.ts index bfce470be6..a064e6c4cc 100644 --- a/packages/db/tests/query/case-when.test-d.ts +++ b/packages/db/tests/query/case-when.test-d.ts @@ -5,7 +5,6 @@ import { eq, gt, include, - leftJoin, toArray, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' @@ -295,9 +294,9 @@ describe(`caseWhen types`, () => { q .from({ user: users }) .join( - leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ), + { post: posts }, + ({ user, post }) => eq(user.id, post.userId), + `left`, ) .select(({ user, post }) => ({ id: user.id, diff --git a/packages/db/tests/query/case-when.test.ts b/packages/db/tests/query/case-when.test.ts index ba2d0b7c2c..a8a5511a31 100644 --- a/packages/db/tests/query/case-when.test.ts +++ b/packages/db/tests/query/case-when.test.ts @@ -6,8 +6,6 @@ import { eq, gt, include, - innerJoin, - leftJoin, toArray, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' @@ -441,9 +439,9 @@ describe(`caseWhen`, () => { q .from({ user: users }) .join( - leftJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.userId), - ), + { post: posts }, + ({ user, post }) => eq(user.id, post.userId), + `left`, ) .select(({ user, post }) => ({ id: user.id, @@ -472,9 +470,10 @@ describe(`caseWhen`, () => { q .from({ user: users }) .join( - innerJoin({ post: posts }, ({ user, post }) => + { post: posts }, + ({ user, post }) => eq(caseWhen(eq(user.active, true), user.id, -1), post.userId), - ), + `inner`, ) .select(({ user, post }) => ({ userId: user.id, diff --git a/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts b/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts index 7d8cf471a0..ae233249bd 100644 --- a/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts +++ b/packages/db/tests/query/cold-join-reconciliation-oracle.test.ts @@ -6,11 +6,7 @@ import { and, createLiveQueryCollection, eq, - fullJoin as fullJoinOperator, gt, - innerJoin, - joinOf, - leftJoin, or, } from '../../src/query/index.js' import { Func } from '../../src/query/ir.js' @@ -150,10 +146,8 @@ async function runColdJoin( const live = createLiveQueryCollection((q) => q .from({ parent: parentSource }) - .join( - innerJoin({ child: children }, ({ parent, child }) => - eq(parent.id, child.parentId), - ), + .innerJoin({ child: children }, ({ parent, child }) => + eq(parent.id, child.parentId), ) .select(({ parent, child }) => ({ id: child.id, @@ -512,10 +506,8 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left }) - .join( - innerJoin({ right }, ({ left: l, right: r }) => - eq(l.value, r.value), - ), + .innerJoin({ right }, ({ left: l, right: r }) => + eq(l.value, r.value), ) .select(({ right: row }) => ({ id: row.id })), }) @@ -578,10 +570,8 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left: binaryLeft }) - .join( - innerJoin({ right: binaryRight }, ({ left, right }) => - eq(left.value, right.value), - ), + .innerJoin({ right: binaryRight }, ({ left, right }) => + eq(left.value, right.value), ) .select(({ right }) => ({ id: right.id })), }) @@ -608,10 +598,8 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left: nullishLeft }) - .join( - fullJoinOperator({ right: nullishRight }, ({ left, right }) => - eq(left.value, right.value), - ), + .fullJoin({ right: nullishRight }, ({ left, right }) => + eq(left.value, right.value), ) .select(({ left, right }) => ({ leftId: left.id, @@ -671,10 +659,8 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left }) - .join( - fullJoinOperator({ right }, ({ left: l, right: r }) => - eq(l.value, r.value), - ), + .fullJoin({ right }, ({ left: l, right: r }) => + eq(l.value, r.value), ) .select(({ left: l, right: r }) => ({ leftId: l.id, @@ -812,10 +798,8 @@ describe.each([`off`, `eager`] as const)( query: (q) => q .from({ left: active }) - .join( - leftJoin({ right: lazy }, ({ left, right }) => - eq(left.binaryId, right.binaryId), - ), + .leftJoin({ right: lazy }, ({ left, right }) => + eq(left.binaryId, right.binaryId), ) .select(({ left, right }) => ({ leftId: left.id, @@ -1140,7 +1124,8 @@ async function runCompoundHistory( q .from({ left }) .join( - joinOf(scenario.join, { right }, ({ left: l, right: r }) => { + { right }, + ({ left: l, right: r }) => { const a = scenario.reversed ? eq(r.a, l.a) : eq(l.a, r.a) const b = scenario.reversed ? eq(l.b, r.b) : eq(r.b, l.b) const c = eq(l.c, r.c) @@ -1148,7 +1133,8 @@ async function runCompoundHistory( if (scenario.width === 2) return and(first, second) if (scenario.nested) return and(first, and(second, c)) return and(first, second, c) - }), + }, + scenario.join, ) .select(({ left: l, right: r }) => ({ leftId: l.id, rightId: r.id })), ) @@ -1420,15 +1406,15 @@ it.each([`or`, `inequality`, `empty`, `nested-invalid`] as const)( await checkWithCleanup( () => { expect(() => - new Query().from({ left }).join( - innerJoin({ right }, ({ left: l, right: r }) => { + new Query() + .from({ left }) + .innerJoin({ right }, ({ left: l, right: r }) => { const equality = eq(l.id, r.id) if (shape === `or`) return or(equality, equality) if (shape === `inequality`) return gt(l.id, r.id) if (shape === `empty`) return new Func(`and`, []) return and(equality, and(equality, gt(l.id, r.id))) }), - ), ).toThrow(JoinConditionMustBeEqualityError) return Promise.resolve() }, diff --git a/packages/db/tests/query/collection-ref-compatibility.test.ts b/packages/db/tests/query/collection-ref-compatibility.test.ts index a28400240e..53bc20f72c 100644 --- a/packages/db/tests/query/collection-ref-compatibility.test.ts +++ b/packages/db/tests/query/collection-ref-compatibility.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { joinRuntime } from '../../src/query/compiler/join-runtime.js' import { includesRuntime } from '../../src/query/live/includes-runtime.js' import { IR, @@ -107,7 +106,6 @@ describe(`CollectionRef public shape`, () => { from: ref(`root`), join: [ { - runtime: joinRuntime, from: ref(`joined`), type: `inner`, on: new IR.Func(`eq`, [ diff --git a/packages/db/tests/query/compiler/basic.test.ts b/packages/db/tests/query/compiler/basic.test.ts index 44ecc08969..f3209ba94a 100644 --- a/packages/db/tests/query/compiler/basic.test.ts +++ b/packages/db/tests/query/compiler/basic.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from 'vitest' import { D2, MultiSet, output } from '@tanstack/db-ivm' -import { joinRuntime } from '../../../src/query/compiler/join-runtime.js' import { compileQuery } from '../../../src/query/compiler/index.js' import { materializeCompilation } from '../../../src/query/live/materialized-pipeline.js' import { CollectionRef, Func, PropRef, Value } from '../../../src/query/ir.js' @@ -213,7 +212,6 @@ describe(`Query2 Compiler`, () => { from: new CollectionRef(usersCollection, userAlias), join: [ { - runtime: joinRuntime, type: `inner`, from: new CollectionRef(postsCollection, postAlias), on: new Func('eq', [ diff --git a/packages/db/tests/query/compiler/lazy-demand.test.ts b/packages/db/tests/query/compiler/lazy-demand.test.ts index 74469b52f4..ddd4d78fa9 100644 --- a/packages/db/tests/query/compiler/lazy-demand.test.ts +++ b/packages/db/tests/query/compiler/lazy-demand.test.ts @@ -1,6 +1,5 @@ import { D2, output } from '@tanstack/db-ivm' import { describe, expect, it } from 'vitest' -import { joinRuntime } from '../../../src/query/compiler/join-runtime.js' import { createCollection } from '../../../src/collection/index.js' import { compileQuery } from '../../../src/query/compiler/index.js' import { CollectionRef, Func, PropRef } from '../../../src/query/ir.js' @@ -31,7 +30,6 @@ function createDemandHarness( from: new CollectionRef(left, `left`), join: [ { - runtime: joinRuntime, type: joinType, from: new CollectionRef(right, `right`), on: diff --git a/packages/db/tests/query/compiler/subqueries.test.ts b/packages/db/tests/query/compiler/subqueries.test.ts index cd3e12b17e..3bf3a6f99f 100644 --- a/packages/db/tests/query/compiler/subqueries.test.ts +++ b/packages/db/tests/query/compiler/subqueries.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest' import { D2, MultiSet, output } from '@tanstack/db-ivm' -import { leftJoin } from '@tanstack/db' import { Query, getQueryIR } from '../../../src/query/builder/index.js' import { compileQuery } from '../../../src/query/compiler/index.js' import { CollectionImpl } from '../../../src/collection/index.js' @@ -224,10 +223,8 @@ describe(`Query2 Subqueries`, () => { // Use the subquery in a join const query = new Query() .from({ issue: issuesCollection }) - .join( - leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ), + .join({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), ) .select(({ issue, activeUser }) => ({ issueId: issue.id, @@ -261,10 +258,8 @@ describe(`Query2 Subqueries`, () => { // Use the subquery in a join const query = new Query() .from({ issue: issuesCollection }) - .join( - leftJoin({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ), + .join({ activeUser: activeUsersQuery }, ({ issue, activeUser }) => + eq(issue.userId, activeUser.id), ) .select(({ issue, activeUser }) => ({ issueId: issue.id, @@ -367,10 +362,8 @@ describe(`Query2 Subqueries`, () => { const outerQuery = new Query() .from({ issue: issuesCollection }) - .join( - leftJoin({ userInfo: middleQuery }, ({ issue, userInfo }) => - eq(issue.userId, userInfo.id), - ), + .join({ userInfo: middleQuery }, ({ issue, userInfo }) => + eq(issue.userId, userInfo.id), ) .select(({ issue, userInfo }) => ({ issueId: issue.id, diff --git a/packages/db/tests/query/compiler/subquery-caching.test.ts b/packages/db/tests/query/compiler/subquery-caching.test.ts index 1e76b898c6..8b2188df20 100644 --- a/packages/db/tests/query/compiler/subquery-caching.test.ts +++ b/packages/db/tests/query/compiler/subquery-caching.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest' import { D2 } from '@tanstack/db-ivm' -import { joinRuntime } from '../../../src/query/compiler/join-runtime.js' import { compileQuery } from '../../../src/query/compiler/index.js' import { queriesMatchForCaching } from '../../../src/query/compiler/query-equivalence.js' import { @@ -105,7 +104,6 @@ describe(`Subquery Caching`, () => { from: new QueryRef(subquery, `main_users`), join: [ { - runtime: joinRuntime, type: `inner`, from: new QueryRef(subquery, `joined_users`), // Same subquery object reference on: new Func('eq', [ @@ -382,7 +380,6 @@ describe(`Subquery Caching`, () => { from: new QueryRef(innerSubquery, `inner1`), join: [ { - runtime: joinRuntime, type: `left`, from: new QueryRef(innerSubquery, `inner2`), // Same innerSubquery on: new Func('eq', [ @@ -397,7 +394,6 @@ describe(`Subquery Caching`, () => { from: new QueryRef(middleSubquery, `middle`), join: [ { - runtime: joinRuntime, type: `inner`, from: new QueryRef(innerSubquery, `direct`), // innerSubquery again at top level on: new Func('eq', [ diff --git a/packages/db/tests/query/composables.test.ts b/packages/db/tests/query/composables.test.ts index 9bc70ad30d..e5744dde63 100644 --- a/packages/db/tests/query/composables.test.ts +++ b/packages/db/tests/query/composables.test.ts @@ -5,7 +5,6 @@ import { createLiveQueryCollection, eq, gt, - innerJoin, lower, lt, lte, @@ -271,9 +270,9 @@ describe(`Composables`, () => { q .from({ user: usersCollection }) .join( - innerJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.authorId), - ), + { post: postsCollection }, + ({ user, post }) => eq(user.id, post.authorId), + `inner`, ) .where(userIsActive) .where(postIsPublished) @@ -425,9 +424,9 @@ describe(`Composables`, () => { q .from({ u: usersCollection }) .join( - innerJoin({ p: postsCollection }, ({ u, p }) => - eq(u.id, p.authorId), - ), + { p: postsCollection }, + ({ u, p }) => eq(u.id, p.authorId), + `inner`, ) .where(joinedUserPostPredicate) .select(joinedSelect), diff --git a/packages/db/tests/query/distinct.test.ts b/packages/db/tests/query/distinct.test.ts index 15c87ce2b4..6a09460c34 100644 --- a/packages/db/tests/query/distinct.test.ts +++ b/packages/db/tests/query/distinct.test.ts @@ -1,9 +1,5 @@ import { beforeEach, describe, expect, test } from 'vitest' -import { - concat, - createLiveQueryCollection, - leftJoin, -} from '../../src/query/index.js' +import { concat, createLiveQueryCollection } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import { DistinctRequiresSelectError } from '../../src/errors' @@ -701,11 +697,9 @@ function createDistinctTests(autoIndex: `off` | `eager`): void { q .from({ users: usersCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ users, departments }) => - eq(users.department, departments.id), - ), + { departments: departmentsCollection }, + ({ users, departments }) => + eq(users.department, departments.id), ) .where(({ users }) => eq(users.active, true)) .select(({ departments }) => ({ diff --git a/packages/db/tests/query/findone-joins.test-d.ts b/packages/db/tests/query/findone-joins.test-d.ts index ef455efbe5..9f76853dd6 100644 --- a/packages/db/tests/query/findone-joins.test-d.ts +++ b/packages/db/tests/query/findone-joins.test-d.ts @@ -1,12 +1,5 @@ import { describe, expectTypeOf, test } from 'vitest' -import { - createLiveQueryCollection, - eq, - fullJoin, - innerJoin, - leftJoin, - rightJoin, -} from '../../src/query/index.js' +import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -52,11 +45,9 @@ describe(`findOne() with joins`, () => { .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) .orderBy(({ todo }) => todo.order, `asc`) - .join( - leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ), + .leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), ) .findOne(), }) @@ -77,11 +68,9 @@ describe(`findOne() with joins`, () => { q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) - .join( - innerJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ), + .innerJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), ) .findOne(), }) @@ -103,11 +92,9 @@ describe(`findOne() with joins`, () => { .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) .findOne() - .join( - leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ), + .leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), ), }) @@ -127,11 +114,9 @@ describe(`findOne() with joins`, () => { q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) - .join( - rightJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ), + .rightJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), ) .findOne(), }) @@ -152,11 +137,9 @@ describe(`findOne() with joins`, () => { q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) - .join( - fullJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ), + .fullJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), ) .findOne(), }) @@ -190,16 +173,12 @@ describe(`findOne() with joins`, () => { query: (q) => q .from({ todo: todoCollection }) - .join( - leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ), + .leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), ) - .join( - innerJoin({ tag: todoTagsCollection }, ({ todo, tag }) => - eq(todo.id, tag.todoId), - ), + .innerJoin({ tag: todoTagsCollection }, ({ todo, tag }) => + eq(todo.id, tag.todoId), ) .findOne(), }) @@ -220,11 +199,9 @@ describe(`findOne() with joins`, () => { query: (q) => q .from({ todo: todoCollection }) - .join( - leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ), + .leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), ) .select(({ todo, todoOptions }) => ({ todoText: todo.text, @@ -248,11 +225,9 @@ describe(`findOne() with joins`, () => { query: (q) => q .from({ todo: todoCollection }) - .join( - leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ), + .leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), ) .findOne() .select(({ todo, todoOptions }) => ({ @@ -278,11 +253,9 @@ describe(`findOne() with joins`, () => { .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.id, `test-id`)) .orderBy(({ todo }) => todo.order, `asc`) - .join( - leftJoin( - { todoOptions: todoOptionsCollection }, - ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), - ), + .leftJoin( + { todoOptions: todoOptionsCollection }, + ({ todo, todoOptions }) => eq(todo.id, todoOptions.todoId), ) .limit(1), }) diff --git a/packages/db/tests/query/functional-variants.test-d.ts b/packages/db/tests/query/functional-variants.test-d.ts index 207c83c1f4..b26a78dc96 100644 --- a/packages/db/tests/query/functional-variants.test-d.ts +++ b/packages/db/tests/query/functional-variants.test-d.ts @@ -7,7 +7,6 @@ import { eq, gt, include, - leftJoin, materialize, toArray, } from '../../src/query/index.js' @@ -273,10 +272,8 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .fn.select((row) => ({ employeeInfo: `${row.user.name} works in ${row.dept?.name || `Unknown`}`, @@ -310,10 +307,8 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .fn.where( (row) => @@ -338,10 +333,8 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .fn.where((row) => row.user.active) .fn.where((row) => row.user.salary > 60000) @@ -539,10 +532,8 @@ describe(`Functional Variants Types`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .groupBy(({ dept }) => dept.name) .fn.having((row) => row.dept?.name !== `HR`) diff --git a/packages/db/tests/query/functional-variants.test.ts b/packages/db/tests/query/functional-variants.test.ts index 0b687849f5..8833be095e 100644 --- a/packages/db/tests/query/functional-variants.test.ts +++ b/packages/db/tests/query/functional-variants.test.ts @@ -6,7 +6,6 @@ import { eq, gt, include, - leftJoin, materialize, toArray, } from '../../src/query/index.js' @@ -194,10 +193,8 @@ describe(`Functional Variants Query`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .fn.select((row) => ({ employeeInfo: `${row.user.name} works in ${row.dept?.name || `Unknown`}`, @@ -870,10 +867,8 @@ describe(`Functional Variants Query`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .join({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .fn.where((row) => row.user.active) .fn.where((row) => row.user.salary > 60000) diff --git a/packages/db/tests/query/generic-collection.test-d.ts b/packages/db/tests/query/generic-collection.test-d.ts index 07ef63c117..553b5f975b 100644 --- a/packages/db/tests/query/generic-collection.test-d.ts +++ b/packages/db/tests/query/generic-collection.test-d.ts @@ -1,9 +1,5 @@ import { describe, test } from 'vitest' -import { - createLiveQueryCollection, - eq, - leftJoin, -} from '../../src/query/index.js' +import { createLiveQueryCollection, eq } from '../../src/query/index.js' import type { Collection } from '../../src/collection/index.js' // Regression tests for https://github.com/TanStack/db/issues/1677 @@ -46,10 +42,8 @@ describe(`queries over generic collection row types`, () => { const sub = q.from({ a }) return q .from({ b }) - .join( - leftJoin({ sub }, ({ b: bRef, sub: subRef }) => - eq(bRef.id, subRef.id), - ), + .leftJoin({ sub }, ({ b: bRef, sub: subRef }) => + eq(bRef.id, subRef.id), ) }) } diff --git a/packages/db/tests/query/group-by.test.ts b/packages/db/tests/query/group-by.test.ts index 9f8522c99d..0021d933a0 100644 --- a/packages/db/tests/query/group-by.test.ts +++ b/packages/db/tests/query/group-by.test.ts @@ -11,7 +11,6 @@ import { Temporal } from 'temporal-polyfill' import { createLiveQueryCollection, groupKeys, - leftJoin, toArray, } from '../../src/query/index.js' import { @@ -2423,10 +2422,8 @@ function createGroupByTests(autoIndex: `off` | `eager`): void { return q .from({ customer: customersCollection }) - .join( - leftJoin({ oc: orderCountSubquery }, ({ customer, oc }) => - eq(customer.id, oc.customer_id), - ), + .leftJoin({ oc: orderCountSubquery }, ({ customer, oc }) => + eq(customer.id, oc.customer_id), ) .select(({ customer, oc }) => ({ name: customer.name, diff --git a/packages/db/tests/query/identity-output-shape-oracle.test.ts b/packages/db/tests/query/identity-output-shape-oracle.test.ts index 69a1f916cc..e332be63bc 100644 --- a/packages/db/tests/query/identity-output-shape-oracle.test.ts +++ b/packages/db/tests/query/identity-output-shape-oracle.test.ts @@ -3,13 +3,11 @@ import { D2, MultiSet, output, serializeValue } from '@tanstack/db-ivm' import { Temporal } from 'temporal-polyfill' import fc from 'fast-check' import { describe, expect, it } from 'vitest' -import { joinRuntime } from '../../src/query/compiler/join-runtime.js' import { Query, and, createLiveQueryCollection, eq, - innerJoin, } from '../../src/query/index.js' import { getQueryIR } from '../../src/query/builder/query-ir.js' import { createCollection } from '../../src/collection/index.js' @@ -157,7 +155,6 @@ async function runShapes( from: user, join: [ { - runtime: joinRuntime, type: 'inner', from: post, on: new Func('eq', [ @@ -670,16 +667,14 @@ describe(`compound join identity and output`, () => { ) => { const joined = new Query() .from({ left }) - .join( - innerJoin({ right }, ({ left: l, right: r }) => { - const a = eq(l.a, r.a) - const b = eq(l.b, variant === `different` ? r.c : r.b) - if (variant === `reordered`) - return and(eq(r.b, l.b), eq(r.a, l.a)) - if (variant === `nested`) return and(b, and(a, b)) - return and(a, b) - }), - ) + .innerJoin({ right }, ({ left: l, right: r }) => { + const a = eq(l.a, r.a) + const b = eq(l.b, variant === `different` ? r.c : r.b) + if (variant === `reordered`) + return and(eq(r.b, l.b), eq(r.a, l.a)) + if (variant === `nested`) return and(b, and(a, b)) + return and(a, b) + }) .select(({ left: l, right: r }) => ({ leftId: l.id, rightId: r.id, @@ -703,10 +698,8 @@ describe(`compound join identity and output`, () => { } const query = new Query() .from({ anchor: left }) - .join( - innerJoin({ result: joined }, ({ anchor, result }) => - eq(anchor.id, result.leftId), - ), + .innerJoin({ result: joined }, ({ anchor, result }) => + eq(anchor.id, result.leftId), ) .select(({ result }) => ({ leftId: result.leftId, diff --git a/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts b/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts index 16bf78a9ae..5f4e9d49fa 100644 --- a/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts +++ b/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts @@ -7,10 +7,7 @@ import { createLiveQueryCollection, eq, gt, - innerJoin, isUndefined, - joinOf, - leftJoin, multiply, toArray, } from '../../src/query/index.js' @@ -239,20 +236,16 @@ function buildQuery( return toArray( new Query() .unionAll(left, right) - .join( - innerJoin({ anchor: locks.collection }, ({ lockId, anchor }) => - eq(lockId, anchor.id), - ), + .innerJoin({ anchor: locks.collection }, ({ lockId, anchor }) => + eq(lockId, anchor.id), ) .where(({ anchor }) => eq(anchor.id, parent.id)) .select(({ id, lockId }) => ({ id, lockId })), ) } return toArray( - base().join( - innerJoin({ other: locks.collection }, (context: Context) => - eq(voteAt(context, alias).lockId, (context.other as Lock).id), - ), + base().innerJoin({ other: locks.collection }, (context: Context) => + eq(voteAt(context, alias).lockId, (context.other as Lock).id), ), ) })() @@ -469,10 +462,8 @@ describe(`captured alias scope oracle`, () => { return createLiveQueryCollection({ query: new Query() .from({ manager: managers.collection }) - .join( - innerJoin({ helper: shared }, ({ manager, helper }) => - eq(manager.id, helper.id), - ), + .innerJoin({ helper: shared }, ({ manager, helper }) => + eq(manager.id, helper.id), ) .select(({ manager }) => reportsFor(manager as unknown as Employee), @@ -632,10 +623,8 @@ describe(`captured alias scope oracle`, () => { const live = createLiveQueryCollection({ query: new Query() .unionAll(first, second) - .join( - innerJoin({ anchor: source.collection }, ({ id, anchor }) => - eq(id, anchor.id), - ), + .innerJoin({ anchor: source.collection }, ({ id, anchor }) => + eq(id, anchor.id), ) .select(({ anchor }) => ({ id: anchor.id, @@ -676,10 +665,8 @@ describe(`captured alias scope oracle`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ wrapped: inner }) - .join( - innerJoin({ anchor: source.collection }, ({ wrapped, anchor }) => - eq(wrapped.id, anchor.id), - ), + .innerJoin({ anchor: source.collection }, ({ wrapped, anchor }) => + eq(wrapped.id, anchor.id), ) .select(({ anchor }) => ({ id: anchor.id, @@ -816,11 +803,9 @@ describe(`captured alias scope oracle`, () => { children: toArray( new Query() .from({ lock: votes.collection }) - .join( - innerJoin( - { other: locks.collection }, - ({ lock: child, other }) => eq(child.lockId, other.id), - ), + .innerJoin( + { other: locks.collection }, + ({ lock: child, other }) => eq(child.lockId, other.id), ) .where(({ lock: child, other }) => eq(child.lockId, other.id)) .where(({ lock: child }) => eq(child.lockId, parent.id)) @@ -1305,10 +1290,8 @@ describe(`outer filters stay with their source under alias shadowing`, () => { form === `join` ? createLiveQueryCollection({ query: root - .join( - leftJoin({ w: childQuery }, ({ u, w }) => - eq(u.linkId, w.id), - ), + .leftJoin({ w: childQuery }, ({ u, w }) => + eq(u.linkId, w.id), ) .select(({ u, w }) => ({ id: u.id, joinedId: w.id })), }) @@ -1454,10 +1437,8 @@ describe(`captured projections keep their binding during predicate pushdown`, () matches: toArray( new Query() .from({ q: inner }) - .join( - innerJoin({ a: anchors.collection }, ({ q, a }) => - eq(q.id, a.id), - ), + .innerJoin({ a: anchors.collection }, ({ q, a }) => + eq(q.id, a.id), ) .where(({ q }) => eq(q.parentId, parent.id)) .where(({ q }) => eq(q.rank, `A`)) @@ -1724,10 +1705,8 @@ describe(`group keys through nested source filtering`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ x: inner }) - .join( - innerJoin({ t: tags.collection }, ({ x, t }) => - eq(x.id, t.sourceId), - ), + .innerJoin({ t: tags.collection }, ({ x, t }) => + eq(x.id, t.sourceId), ) .where(({ x }) => eq(x.category, `A`)) .groupBy(({ x }) => x.category) @@ -2008,7 +1987,7 @@ describe(`lazy join demand across nested alias scopes`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ a: anchors.collection }) - .join(leftJoin({ w: inner }, ({ a, w }) => eq(a.userId, w.id))) + .leftJoin({ w: inner }, ({ a, w }) => eq(a.userId, w.id)) .select(({ a, w }) => ({ anchorId: a.id, userId: w.id, @@ -2132,10 +2111,8 @@ describe(`lexical bindings through recursive joins and selections`, () => { return createLiveQueryCollection({ query: new Query() .from({ manager: managers }) - .join( - leftJoin({ user: people.collection }, ({ manager, user }) => - eq(manager.id, user.managerId), - ), + .leftJoin({ user: people.collection }, ({ manager, user }) => + eq(manager.id, user.managerId), ) .where(({ user }) => eq(user.managerId, 1)) .select(({ manager, user }) => ({ @@ -2207,10 +2184,8 @@ describe(`lexical bindings through recursive joins and selections`, () => { return createLiveQueryCollection({ query: new Query() .from({ manager: inner }) - .join( - leftJoin({ user: users.collection }, ({ manager, user }) => - eq(manager.id, user.managerId), - ), + .leftJoin({ user: users.collection }, ({ manager, user }) => + eq(manager.id, user.managerId), ) .select(({ manager, user }) => ({ managerId: manager.id, @@ -2327,12 +2302,10 @@ describe(`lexical bindings through recursive joins and selections`, () => { children: toArray( new Query() .from({ [childAlias]: children.collection }) - .join( - innerJoin({ anchor: anchorQuery }, (context: Context) => - eq( - (context[childAlias] as { anchorId: number }).anchorId, - (context.anchor as { id: number }).id, - ), + .innerJoin({ anchor: anchorQuery }, (context: Context) => + eq( + (context[childAlias] as { anchorId: number }).anchorId, + (context.anchor as { id: number }).id, ), ) .where((context: Context) => @@ -2404,15 +2377,13 @@ describe(`lexical bindings through recursive joins and selections`, () => { children: toArray( new Query() .from({ comment: comments.collection }) - .join( - leftJoin( - { [joinAlias]: anchors.collection }, - (context: Context) => - eq( - (context.comment as { anchorId: number }).anchorId, - (context[joinAlias] as { id: number }).id, - ), - ), + .leftJoin( + { [joinAlias]: anchors.collection }, + (context: Context) => + eq( + (context.comment as { anchorId: number }).anchorId, + (context[joinAlias] as { id: number }).id, + ), ) .where((context: Context) => eq( @@ -2884,11 +2855,9 @@ describe(`nested source placement preserves captured outer rows`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ manager: managers.collection }) - .join( - innerJoin( - { user: assignments.collection }, - ({ manager, user }) => eq(manager.id, user.managerId), - ), + .innerJoin( + { user: assignments.collection }, + ({ manager, user }) => eq(manager.id, user.managerId), ) .select(({ manager, user }) => { const project = (context: Context) => { @@ -2914,15 +2883,13 @@ describe(`nested source placement preserves captured outer rows`, () => { .select(project) : new Query() .from({ anchor: anchors.collection }) - .join( - innerJoin( - { [alias]: details.collection }, - (context: Context) => - eq( - (context[alias] as Detail).managerId, - (context.anchor as { id: number }).id, - ), - ), + .innerJoin( + { [alias]: details.collection }, + (context: Context) => + eq( + (context[alias] as Detail).managerId, + (context.anchor as { id: number }).id, + ), ) .where((context: Context) => eq( @@ -3217,9 +3184,7 @@ describe(`outer joins preserve absent source bindings under shadowing`, () => { ) const anchorSource = sourceForm === `direct` ? anchors.collection : scopedAnchors - const joined = main.join( - joinOf(kind, { anchor: anchorSource }, on), - ) + const joined = main.join({ anchor: anchorSource }, on, kind) expect(getQueryIR(joined).join?.[0]?.from.type).toBe( sourceForm === `direct` ? `collectionRef` : `queryRef`, ) @@ -3481,31 +3446,27 @@ describe(`chained joins keep an absent local source absent`, () => { rows: toArray( new Query() .from({ comment: comments.collection }) - .join( - leftJoin( - { [alias]: localIssues.collection }, - (context: Context) => - eq( - (context.comment as Comment).localIssueId, - (context[alias] as Issue).id, - ), - ), + .leftJoin( + { [alias]: localIssues.collection }, + (context: Context) => + eq( + (context.comment as Comment).localIssueId, + (context[alias] as Issue).id, + ), ) - .join( - leftJoin({ tag: tagSource }, (context: Context) => - routeCause === `queryRef` - ? eq( + .leftJoin({ tag: tagSource }, (context: Context) => + routeCause === `queryRef` + ? eq( + (context.comment as Comment).tagId, + (context.tag as Tag).id, + ) + : eq( + add( (context.comment as Comment).tagId, - (context.tag as Tag).id, - ) - : eq( - add( - (context.comment as Comment).tagId, - parent.id, - ), - add((context.tag as Tag).id, parent.id), + parent.id, ), - ), + add((context.tag as Tag).id, parent.id), + ), ) .where((context: Context) => eq((context.comment as Comment).issueId, parent.id), @@ -3687,10 +3648,8 @@ describe(`joined QueryRef lazy targets retain lexical source identity`, () => { }) const joined = new Query() .from({ [alias]: posts.collection }) - .join( - innerJoin({ tag: tags.collection }, (context: Context) => - eq((context[alias] as Post).tagId, (context.tag as Tag).id), - ), + .innerJoin({ tag: tags.collection }, (context: Context) => + eq((context[alias] as Post).tagId, (context.tag as Tag).id), ) .select((context: Context) => ({ tagId: (context.tag as Tag).id, @@ -3700,7 +3659,7 @@ describe(`joined QueryRef lazy targets retain lexical source identity`, () => { const live = createLiveQueryCollection({ query: new Query() .from({ u: users.collection }) - .join(leftJoin({ p: joined }, ({ u, p }) => eq(u.tagId, p.tagId))) + .leftJoin({ p: joined }, ({ u, p }) => eq(u.tagId, p.tagId)) .select(({ u, p }) => ({ id: u.id, name: u.name, @@ -3955,26 +3914,22 @@ describe(`mixed outer-join chains retain absent source roles`, () => { new Query() .from({ [mainAlias]: mains.collection }) .join( - joinOf( - first, - { [anchorAlias]: anchors.collection }, - (context: Context) => - eq( - (context[mainAlias] as Main).anchorId, - (context[anchorAlias] as Anchor).id, - ), - ), + { [anchorAlias]: anchors.collection }, + (context: Context) => + eq( + (context[mainAlias] as Main).anchorId, + (context[anchorAlias] as Anchor).id, + ), + first, ) .join( - joinOf( - second, - { tag: tags.collection }, - (context: Context) => - eq( - (context[anchorAlias] as Anchor).id, - (context.tag as Tag).anchorId, - ), - ), + { tag: tags.collection }, + (context: Context) => + eq( + (context[anchorAlias] as Anchor).id, + (context.tag as Tag).anchorId, + ), + second, ) .where((context: Context) => correlation === `main` @@ -4169,27 +4124,25 @@ describe(`captured ancestor join operands retain their bindings`, () => { rows: toArray( new Query() .from({ child: children.collection }) - .join( - innerJoin( - { [alias]: targets.collection }, - (context: Context) => { - const child = context.child as Child - const target = context[alias] as Target - const mainKey = - ancestorSide === `main` - ? add(child.base, ancestor.offset) - : ancestorSide === `soleMain` - ? ancestor.offset - : child.base - const joinedKey = - ancestorSide === `joined` - ? add(target.id, ancestor.offset) - : target.id - return order === `childFirst` - ? eq(mainKey, joinedKey) - : eq(joinedKey, mainKey) - }, - ), + .innerJoin( + { [alias]: targets.collection }, + (context: Context) => { + const child = context.child as Child + const target = context[alias] as Target + const mainKey = + ancestorSide === `main` + ? add(child.base, ancestor.offset) + : ancestorSide === `soleMain` + ? ancestor.offset + : child.base + const joinedKey = + ancestorSide === `joined` + ? add(target.id, ancestor.offset) + : target.id + return order === `childFirst` + ? eq(mainKey, joinedKey) + : eq(joinedKey, mainKey) + }, ) .where((context: Context) => eq((context.child as Child).parentId, ancestor.id), @@ -4349,10 +4302,8 @@ describe(`join operands reject captured sources outside lexical scope`, () => { const child = (operand: number) => new Query() .from({ local: local.collection }) - .join( - innerJoin({ joined: joined.collection }, ({ joined: target }) => - eq(operand, target.id), - ), + .innerJoin({ joined: joined.collection }, ({ joined: target }) => + eq(operand, target.id), ) .select(({ local: own, joined: target }) => ({ localId: own.id, @@ -4385,11 +4336,9 @@ describe(`join operands reject captured sources outside lexical scope`, () => { const localControl = createLiveQueryCollection({ query: new Query() .from({ local: local.collection }) - .join( - innerJoin( - { joined: joined.collection }, - ({ local: own, joined: target }) => eq(own.id, target.id), - ), + .innerJoin( + { joined: joined.collection }, + ({ local: own, joined: target }) => eq(own.id, target.id), ) .select(({ local: own, joined: target }) => ({ localId: own.id, @@ -4984,11 +4933,9 @@ describe(`include joins reject sources hidden inside parent operands`, () => { rows: toArray( new Query() .from({ local: local.collection }) - .join( - innerJoin( - { joined: joined.collection }, - ({ joined: target }) => eq(hiddenRef, target.id), - ), + .innerJoin( + { joined: joined.collection }, + ({ joined: target }) => eq(hiddenRef, target.id), ) .where(({ local: row }) => eq(row.id, parentId)) .select(({ local: row, joined: target }) => ({ @@ -5076,19 +5023,15 @@ describe(`joined subqueries reject captured sibling sources`, () => { }) const joinedQuery = new Query() .from({ [alias]: inner.collection }) - .join( - innerJoin({ probe: probe.collection }, ({ probe: row }) => - eq(siblingRef, row.id), - ), + .innerJoin({ probe: probe.collection }, ({ probe: row }) => + eq(siblingRef, row.id), ) .select((context: Context) => ({ id: (context[alias] as Row).id, })) const child = base - .join( - innerJoin({ nested: joinedQuery }, ({ local: row, nested }) => - eq(row.id, nested.id), - ), + .innerJoin({ nested: joinedQuery }, ({ local: row, nested }) => + eq(row.id, nested.id), ) .select(({ local: row, nested }) => ({ localId: row.id, @@ -5183,10 +5126,8 @@ describe(`joined subqueries reject captured sibling sources`, () => { .where((context: Context) => eq((context[alias] as Row).id, ancestor.id), ) - .join( - innerJoin({ probe: probe.collection }, ({ probe: row }) => - eq(ancestor.id, row.id), - ), + .innerJoin({ probe: probe.collection }, ({ probe: row }) => + eq(ancestor.id, row.id), ) .select((context: Context) => ({ id: (context[alias] as Row).id, @@ -5197,11 +5138,9 @@ describe(`joined subqueries reject captured sibling sources`, () => { rows: toArray( new Query() .from({ local: main.collection }) - .join( - innerJoin( - { nested: joinedQuery }, - ({ local: row, nested }) => eq(row.id, nested.id), - ), + .innerJoin( + { nested: joinedQuery }, + ({ local: row, nested }) => eq(row.id, nested.id), ) .where(({ local: row }) => eq(row.id, ancestor.id)) .select(({ local: row, nested }) => ({ @@ -5268,10 +5207,8 @@ test(`mixed bound and unbound same-alias join references name separate sources`, rows: toArray( new Query() .from({ child: child.collection }) - .join( - innerJoin({ issue: local.collection }, () => - eq(ancestor.id, new PropRef([`issue`, `id`])), - ), + .innerJoin({ issue: local.collection }, () => + eq(ancestor.id, new PropRef([`issue`, `id`])), ) .where(({ child: row }) => eq(row.parentId, ancestor.id)) .select(({ child: row, issue }) => ({ diff --git a/packages/db/tests/query/includes-collection-oracle.property.test.ts b/packages/db/tests/query/includes-collection-oracle.property.test.ts index 83dbe35226..a0719cca1d 100644 --- a/packages/db/tests/query/includes-collection-oracle.property.test.ts +++ b/packages/db/tests/query/includes-collection-oracle.property.test.ts @@ -9,8 +9,6 @@ import { eq, gt, include, - innerJoin, - leftJoin, lt, materialize, multiply, @@ -1674,10 +1672,8 @@ describe(`Collection-valued includes oracle`, () => { query: (q) => { const joined = q .from({ comment: comments.collection }) - .join( - leftJoin({ user: users.collection }, ({ comment, user }) => - eq(comment.userId, user.id), - ), + .join({ user: users.collection }, ({ comment, user }) => + eq(comment.userId, user.id), ) return mode === `value` ? joined.select(({ comment }) => ({ @@ -3130,10 +3126,8 @@ describe(`Collection-valued includes oracle`, () => { const childRows = () => q .from({ child: children.collection }) - .join( - innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(tag.id, add(child.tagId, parent.offset)), - ), + .innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(tag.id, add(child.tagId, parent.offset)), ) .where(({ child }) => eq(child.parentGroup, parent.group)) .select(({ child, tag }) => ({ diff --git a/packages/db/tests/query/includes-context-transport-oracle.test.ts b/packages/db/tests/query/includes-context-transport-oracle.test.ts index 161519315c..29b8578283 100644 --- a/packages/db/tests/query/includes-context-transport-oracle.test.ts +++ b/packages/db/tests/query/includes-context-transport-oracle.test.ts @@ -10,7 +10,6 @@ import { groupKeys, gt, include, - innerJoin, lt, lte, materialize, @@ -161,10 +160,8 @@ test('keeps dotted ancestor aliases distinct in nested includes', async () => { const live = createLiveQueryCollection((q) => q .from({ 'p.q': dotted.collection }) - .join( - innerJoin({ p: nested.collection }, ({ 'p.q': left, p }) => - eq(left.id, p.id), - ), + .innerJoin({ p: nested.collection }, ({ 'p.q': left, p }) => + eq(left.id, p.id), ) .select(({ 'p.q': left, p }) => ({ id: left.id, @@ -1245,11 +1242,9 @@ async function runRecursiveSourceCell({ case `joined-query-ref`: { const routed = q .from({ anchor: anchors.collection }) - .join( - innerJoin( - { result: buildCandidates(candidates.collection) }, - ({ anchor, result }) => eq(anchor.id, result.id), - ), + .innerJoin( + { result: buildCandidates(candidates.collection) }, + ({ anchor, result }) => eq(anchor.id, result.id), ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ result }) => ({ @@ -1261,10 +1256,8 @@ async function runRecursiveSourceCell({ case `union-branch`: { const routed = q .unionAll(buildLeftCandidates(), buildRightCandidates()) - .join( - innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => - eq(id, anchor.id), - ), + .innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => + eq(id, anchor.id), ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ id, value }) => ({ id, value })) @@ -1409,10 +1402,8 @@ async function runUnionIdentityCell({ })) return q .unionAll(leftRows, rightRows) - .join( - innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => - eq(id, anchor.id), - ), + .innerJoin({ anchor: anchors.collection }, ({ id, anchor }) => + eq(id, anchor.id), ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ id, value }) => ({ id, value })) @@ -1423,12 +1414,10 @@ async function runUnionIdentityCell({ leftCandidate: left.collection, rightCandidate: right.collection, }) - .join( - innerJoin( - { anchor: anchors.collection }, - ({ leftCandidate, rightCandidate, anchor }) => - eq(coalesce(leftCandidate.id, rightCandidate.id), anchor.id), - ), + .innerJoin( + { anchor: anchors.collection }, + ({ leftCandidate, rightCandidate, anchor }) => + eq(coalesce(leftCandidate.id, rightCandidate.id), anchor.id), ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ leftCandidate, rightCandidate }) => ({ @@ -1555,10 +1544,8 @@ async function runDerivedResultCell({ ) const rows = q .from({ anchor: anchors.collection }) - .join( - innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value), - ), + .innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value), ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) @@ -1572,10 +1559,8 @@ async function runDerivedResultCell({ if (boundary === `from-query-ref`) { const rows = q .from({ value: values }) - .join( - innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => - eq(value, anchor.value), - ), + .innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => + eq(value, anchor.value), ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) @@ -1584,10 +1569,8 @@ async function runDerivedResultCell({ const rows = q .from({ anchor: anchors.collection }) - .join( - innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value), - ), + .innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value), ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) @@ -1600,10 +1583,8 @@ async function runDerivedResultCell({ if (boundary === `from-query-ref`) { const rows = q .from({ value: values }) - .join( - innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => - eq(value, anchor.value), - ), + .innerJoin({ anchor: anchors.collection }, ({ value, anchor }) => + eq(value, anchor.value), ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) @@ -1612,10 +1593,8 @@ async function runDerivedResultCell({ const rows = q .from({ anchor: anchors.collection }) - .join( - innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value), - ), + .innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value), ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) @@ -1736,12 +1715,10 @@ async function runQueryRefMetadataCell( })) return q .from({ anchor: anchors.collection }) - .join( - innerJoin( - { candidateResult: projectedCandidates }, - ({ anchor, candidateResult }) => - eq(anchor.candidateId, candidateResult.id), - ), + .innerJoin( + { candidateResult: projectedCandidates }, + ({ anchor, candidateResult }) => + eq(anchor.candidateId, candidateResult.id), ) .select(({ candidateResult }) => candidateResult) }) @@ -1773,12 +1750,10 @@ async function runQueryRefMetadataCell( })) const rows = q .from({ anchor: anchors.collection }) - .join( - innerJoin( - { candidateResult: projectedCandidates }, - ({ anchor, candidateResult }) => - eq(anchor.candidateId, candidateResult.id), - ), + .innerJoin( + { candidateResult: projectedCandidates }, + ({ anchor, candidateResult }) => + eq(anchor.candidateId, candidateResult.id), ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ candidateResult }) => candidateResult) @@ -1837,17 +1812,13 @@ async function runJoinCell({ keySide === `main` ? q .from({ child: children.collection }) - .join( - innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(tag.id, add(child.tagId, parent.offset)), - ), + .innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(tag.id, add(child.tagId, parent.offset)), ) : q .from({ child: children.collection }) - .join( - innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(add(tag.id, parent.offset), child.tagId), - ), + .innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(add(tag.id, parent.offset), child.tagId), ) const routed = ( correlationAttachment === `main` @@ -1966,10 +1937,8 @@ async function runNamespaceCollisionCell({ case `join`: return includeInEveryForm( correlated - .join( - innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(child.id, tag.id), - ), + .innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(child.id, tag.id), ) .select(({ child }) => ({ id: child.id, @@ -2032,10 +2001,8 @@ async function runNamespaceCollisionCell({ case `join`: return includeInEveryForm( correlated - .join( - innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(child.id, tag.id), - ), + .innerJoin({ tag: tags.collection }, ({ child, tag }) => + eq(child.id, tag.id), ) .select(({ child }) => ({ id: child.id, @@ -2209,10 +2176,8 @@ async function runPublicSurfaceCell({ .fn.select(({ candidate }) => candidate.value) const rows = q .from({ anchor: anchors.collection }) - .join( - innerJoin({ value: values }, ({ anchor, value }) => - eq(anchor.value, value), - ), + .innerJoin({ value: values }, ({ anchor, value }) => + eq(anchor.value, value), ) .where(({ anchor }) => eq(anchor.parentGroup, parent.group)) .select(({ anchor, value }) => ({ id: anchor.id, value })) @@ -2282,10 +2247,9 @@ async function runPublicSurfaceCell({ return { id: parent.id, ...includeInEveryForm(rows) } } - const rows = correlated.join( - innerJoin({ tag: tags.collection }, ({ child, tag }) => - eq(child.id, tag.childId), - ), + const rows = correlated.innerJoin( + { tag: tags.collection }, + ({ child, tag }) => eq(child.id, tag.childId), ) return { id: parent.id, ...includeInEveryForm(rows) } }), @@ -2742,22 +2706,18 @@ describe(`compound join parent context`, () => { ) const joined = boundary === `direct` - ? base.join( - innerJoin({ peer: peers.collection }, joinCondition), - ) - : base.join( - innerJoin( - { - peer: q - .from({ peer: peers.collection }) - .select(({ peer }) => ({ - id: peer.id, - a: peer.a, - b: peer.b, - })), - }, - joinCondition, - ), + ? base.innerJoin({ peer: peers.collection }, joinCondition) + : base.innerJoin( + { + peer: q + .from({ peer: peers.collection }) + .select(({ peer }) => ({ + id: peer.id, + a: peer.a, + b: peer.b, + })), + }, + joinCondition, ) const rows = joined.select(({ child, peer }) => ({ id: child.id, diff --git a/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts b/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts index 327d96662c..4f536147c7 100644 --- a/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts +++ b/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts @@ -7,7 +7,6 @@ import { createLiveQueryCollection, eq, isNull, - leftJoin, lt, not, queryOnce, @@ -542,10 +541,8 @@ function createFlatQuery( query: (q) => q .from({ parent: parents }) - .join( - leftJoin({ child: children }, ({ parent, child }) => - eq(parent.group, child.parentGroup), - ), + .leftJoin({ child: children }, ({ parent, child }) => + eq(parent.group, child.parentGroup), ) .select(({ parent, child }) => ({ parentId: parent.id, diff --git a/packages/db/tests/query/includes-lazy-loading.test.ts b/packages/db/tests/query/includes-lazy-loading.test.ts index 708d7bb977..00371d4cb1 100644 --- a/packages/db/tests/query/includes-lazy-loading.test.ts +++ b/packages/db/tests/query/includes-lazy-loading.test.ts @@ -5,7 +5,6 @@ import { eq, gte, include, - leftJoin, toArray, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' @@ -193,10 +192,8 @@ describe(`includes lazy loading`, () => { matches: toArray( q .from({ left: items }) - .join( - leftJoin({ right: items }, ({ left, right }) => - eq(left.peerId, right.id), - ), + .join({ right: items }, ({ left, right }) => + eq(left.peerId, right.id), ) .where(({ right }) => eq(right.rootId, root.id)) .select(({ left, right }) => ({ diff --git a/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts b/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts index c19a267c9c..531b46defa 100644 --- a/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts +++ b/packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts @@ -3,7 +3,6 @@ import { Query, createLiveQueryCollection, eq, - joinOf, toArray, } from '../../src/query/index.js' import { BasicIndex } from '../../src/indexes/basic-index.js' @@ -155,11 +154,9 @@ describe(`direct outer-join route work`, () => { new Query() .from({ main: mains.collection }) .join( - joinOf( - kind, - { anchor: anchors.collection }, - ({ main, anchor }) => eq(main.anchorId, anchor.id), - ), + { anchor: anchors.collection }, + ({ main, anchor }) => eq(main.anchorId, anchor.id), + kind, ) .where(({ main }) => eq(main.parentId, parent.id)) .select(({ main, anchor }) => ({ diff --git a/packages/db/tests/query/includes-publication-oracle.test.ts b/packages/db/tests/query/includes-publication-oracle.test.ts index e78293a834..9550aa4c9d 100644 --- a/packages/db/tests/query/includes-publication-oracle.test.ts +++ b/packages/db/tests/query/includes-publication-oracle.test.ts @@ -4,7 +4,6 @@ import { afterEach, describe, expect, it } from 'vitest' import { createLiveQueryCollection, eq, - innerJoin, materialize, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' @@ -136,11 +135,10 @@ function createLayeredQuery( q1Shape === `direct` ? source : source.join( - innerJoin( - { metadata: metadata.collection }, - ({ item, metadata: rowMetadata }) => - eq(item.id, rowMetadata.parentId), - ), + { metadata: metadata.collection }, + ({ item, metadata: rowMetadata }) => + eq(item.id, rowMetadata.parentId), + `inner`, ) return parentRows.select(({ item }) => ({ diff --git a/packages/db/tests/query/includes-query-shape-oracle.test.ts b/packages/db/tests/query/includes-query-shape-oracle.test.ts index 746e78cd97..4291c10808 100644 --- a/packages/db/tests/query/includes-query-shape-oracle.test.ts +++ b/packages/db/tests/query/includes-query-shape-oracle.test.ts @@ -3,7 +3,6 @@ import { describe, expect } from 'vitest' import { createLiveQueryCollection, eq, - innerJoin, materialize, } from '../../src/query/index.js' import { BasicIndex } from '../../src/indexes/basic-index.js' @@ -126,11 +125,9 @@ function createMultiplicityQuery(sources: MultiplicitySources) { query: (q) => q .from({ parent: sources.parents.collection }) - .join( - innerJoin( - { child: sources.children.collection }, - ({ parent, child }) => eq(parent.id, child.parentId), - ), + .innerJoin( + { child: sources.children.collection }, + ({ parent, child }) => eq(parent.id, child.parentId), ) .select(({ parent }) => ({ id: parent.id })), getKey: (row) => row.id, @@ -234,11 +231,9 @@ function createCorrelationQuery( .select(({ part }) => { const joined = q .from({ production: sources.productions.collection }) - .join( - innerJoin( - { order: sources.orders.collection }, - ({ production, order }) => eq(production.orderId, order.id), - ), + .innerJoin( + { order: sources.orders.collection }, + ({ production, order }) => eq(production.orderId, order.id), ) const correlated = target === `joined` diff --git a/packages/db/tests/query/includes-scope-identity-oracle.ts b/packages/db/tests/query/includes-scope-identity-oracle.ts index fae90bbfff..613244e91e 100644 --- a/packages/db/tests/query/includes-scope-identity-oracle.ts +++ b/packages/db/tests/query/includes-scope-identity-oracle.ts @@ -3,9 +3,6 @@ import { and, createLiveQueryCollection, eq, - innerJoin, - joinOf, - leftJoin, materialize, not, toArray, @@ -617,10 +614,8 @@ export function createScopedQuery( branch(n.activeBranch!, true), branch(n.inactiveBranch!, false), ) - .join( - innerJoin({ [n.anchor!]: sources.parts.collection }, (c: Context) => - eq(c.id, c[n.anchor!].id), - ), + .innerJoin({ [n.anchor!]: sources.parts.collection }, (c: Context) => + eq(c.id, c[n.anchor!].id), ) .select((c: Context) => ({ id: c.id, @@ -653,9 +648,9 @@ export function createScopedQuery( } as any) as any ) .join( - joinOf(shape.wrapperJoin, { [n.joinSub!]: clientOne }, (c: Context) => - eq(c[n.joinSub!].partId, c[n.wrapped!].id), - ), + { [n.joinSub!]: clientOne }, + (c: Context) => eq(c[n.joinSub!].partId, c[n.wrapped!].id), + shape.wrapperJoin, ) .select((c: Context) => ({ id: c[n.wrapped!].id, @@ -668,18 +663,16 @@ export function createScopedQuery( let sub: any = q.from({ [n.sub!]: sources.parts.collection }) if (subquery.joins !== `none`) { sub = sub.join( - joinOf( - subquery.refsJoin, - { [n.joined!]: sources.refs.collection }, - (c: Context) => eq(c[n.joined!].partId, c[n.sub!].id), - ), + { [n.joined!]: sources.refs.collection }, + (c: Context) => eq(c[n.joined!].partId, c[n.sub!].id), + subquery.refsJoin, ) } if (subquery.joins === `refsThenNotes`) { sub = sub.join( - leftJoin({ [n.noted!]: sources.notes.collection }, (c: Context) => - eq(c[n.noted!].partId, c[n.sub!].id), - ), + { [n.noted!]: sources.notes.collection }, + (c: Context) => eq(c[n.noted!].partId, c[n.sub!].id), + `left`, ) } const predicates = subquery.predicates.map((predicate) => @@ -736,11 +729,9 @@ export function createScopedQuery( branch(n.include!, clientOne), branch(n.includeOther!, (member) => not(clientOne(member))), ) - .join( - innerJoin( - { [n.includeAnchor!]: includeCollection }, - (cc: Context) => eq(cc.memberId, cc[n.includeAnchor!].id), - ), + .innerJoin( + { [n.includeAnchor!]: includeCollection }, + (cc: Context) => eq(cc.memberId, cc[n.includeAnchor!].id), ) .where((cc: Context) => eq(cc[n.includeAnchor!].partId, outer.id)) if (include.clientFilter) { @@ -771,10 +762,9 @@ export function createScopedQuery( : q.from({ [member]: includeCollection }) if (include.body === `joined`) { child = child.join( - innerJoin( - { [n.includeJoin!]: sources.parts.collection }, - (cc: Context) => eq(cc[n.includeJoin!].id, cc[member].partId), - ), + { [n.includeJoin!]: sources.parts.collection }, + (cc: Context) => eq(cc[n.includeJoin!].id, cc[member].partId), + `inner`, ) } child = child.where((cc: Context) => eq(cc[member].partId, outer.id)) diff --git a/packages/db/tests/query/includes-temporal-oracle.test.ts b/packages/db/tests/query/includes-temporal-oracle.test.ts index af7ed12ab9..460ed64d5b 100644 --- a/packages/db/tests/query/includes-temporal-oracle.test.ts +++ b/packages/db/tests/query/includes-temporal-oracle.test.ts @@ -3,7 +3,6 @@ import { describe, expect, it, vi } from 'vitest' import { createLiveQueryCollection, eq, - leftJoin, materialize, queryOnce, toArray, @@ -2986,9 +2985,7 @@ function laggingSourceQuery( })) return q .from({ user: users! }) - .join( - leftJoin({ post: posts }, ({ user, post }) => eq(user.id, post.userId)), - ) + .join({ post: posts }, ({ user, post }) => eq(user.id, post.userId)) .select(({ user, post }) => ({ id: user.id, postId: post.id })) } } diff --git a/packages/db/tests/query/includes-work-counter-oracle.test.ts b/packages/db/tests/query/includes-work-counter-oracle.test.ts index a65c0f6b75..52ac609ede 100644 --- a/packages/db/tests/query/includes-work-counter-oracle.test.ts +++ b/packages/db/tests/query/includes-work-counter-oracle.test.ts @@ -5,8 +5,6 @@ import { eq, inArray, include, - innerJoin, - leftJoin, materialize, toArray, } from '../../src/query/index.js' @@ -325,12 +323,10 @@ async function observeWork({ links: joinTargets ? materialize( selectedLinks - .join( - innerJoin( - { target: sources.terms }, - ({ link, target }) => - eq(link.targetId, target.id), - ), + .innerJoin( + { target: sources.terms }, + ({ link, target }) => + eq(link.targetId, target.id), ) .select(({ link, target }) => ({ id: link.id, @@ -2087,11 +2083,7 @@ describe(`static include demand and setWindow ownership`, () => { ? q.from({ page }) : q .from({ row: rows }) - .join( - leftJoin({ page }, ({ row, page: paged }) => - eq(row.id, paged.id), - ), - ) + .join({ page }, ({ row, page: paged }) => eq(row.id, paged.id)) }) try { await live.preload() diff --git a/packages/db/tests/query/includes.test.ts b/packages/db/tests/query/includes.test.ts index aadc9dff21..bd42ca758c 100644 --- a/packages/db/tests/query/includes.test.ts +++ b/packages/db/tests/query/includes.test.ts @@ -10,7 +10,6 @@ import { gt, gte, include, - innerJoin, materialize, toArray, } from '../../src/query/index.js' @@ -3532,11 +3531,10 @@ describe(`includes subqueries`, () => { q .from({ joinedChild: children }) .join( - innerJoin( - { metadata: metadataRows }, - ({ joinedChild, metadata }) => - eq(joinedChild.metadataId, metadata.id), - ), + { metadata: metadataRows }, + ({ joinedChild, metadata }) => + eq(joinedChild.metadataId, metadata.id), + `inner`, ) .where(({ metadata }) => eq(metadata.groupId, parent.groupId)) .where(({ metadata }) => eq(metadata.tag, parent.tag)) diff --git a/packages/db/tests/query/indexes.test.ts b/packages/db/tests/query/indexes.test.ts index f28daa640e..eb7f4f17a5 100644 --- a/packages/db/tests/query/indexes.test.ts +++ b/packages/db/tests/query/indexes.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it } from 'vitest' -import { innerJoin, leftJoin, rightJoin } from '@tanstack/db' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index' @@ -390,10 +389,8 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join( - leftJoin({ other: secondCollection }, ({ item, other }: any) => - eq(item.status, other.status), - ), + .join({ other: secondCollection }, ({ item, other }: any) => + eq(item.status, other.status), ) .where(({ item }: any) => eq(item.age, 25)) .select(({ item, other }: any) => ({ @@ -502,10 +499,8 @@ describe(`Query Index Optimization`, () => { query: (q: any) => q .from({ item: collection }) - .join( - leftJoin({ other: secondCollection }, ({ item, other }: any) => - eq(item.id, other.id), - ), + .join({ other: secondCollection }, ({ item, other }: any) => + eq(item.id, other.id), ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), @@ -609,9 +604,9 @@ describe(`Query Index Optimization`, () => { q .from({ item: collection }) .join( - innerJoin({ other: secondCollection }, ({ item, other }: any) => - eq(item.id, other.id2), - ), + { other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2), + `inner`, ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), @@ -709,9 +704,9 @@ describe(`Query Index Optimization`, () => { q .from({ item: collection }) .join( - innerJoin({ other: secondCollection }, ({ item, other }: any) => - eq(item.id, other.id2), - ), + { other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2), + `inner`, ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), @@ -804,9 +799,9 @@ describe(`Query Index Optimization`, () => { q .from({ item: collection }) .join( - leftJoin({ other: secondCollection }, ({ item, other }: any) => - eq(item.id, other.id2), - ), + { other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2), + `left`, ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), @@ -924,9 +919,9 @@ describe(`Query Index Optimization`, () => { q .from({ item: collection }) .join( - leftJoin({ other: secondCollection }, ({ item, other }: any) => - eq(item.id, other.id2), - ), + { other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2), + `left`, ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), @@ -1022,9 +1017,9 @@ describe(`Query Index Optimization`, () => { q .from({ item: collection }) .join( - rightJoin({ other: secondCollection }, ({ item, other }: any) => - eq(item.id, other.id2), - ), + { other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2), + `right`, ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), @@ -1116,9 +1111,9 @@ describe(`Query Index Optimization`, () => { q .from({ item: collection }) .join( - rightJoin({ other: secondCollection }, ({ item, other }: any) => - eq(item.id, other.id2), - ), + { other: secondCollection }, + ({ item, other }: any) => eq(item.id, other.id2), + `right`, ) .where(({ item, other }: any) => and(eq(item.status, `active`), eq(other.status, `active`)), diff --git a/packages/db/tests/query/ir-stable-identity-oracle.test.ts b/packages/db/tests/query/ir-stable-identity-oracle.test.ts index 590b5020d1..042f4cbffb 100644 --- a/packages/db/tests/query/ir-stable-identity-oracle.test.ts +++ b/packages/db/tests/query/ir-stable-identity-oracle.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { fc, test as fcTest } from '@fast-check/vitest' import { Temporal } from 'temporal-polyfill' -import { innerJoin, leftJoin } from '@tanstack/db' -import { joinRuntime } from '../../src/query/compiler/join-runtime.js' import { include } from '../../src/query/index.js' import { includesRuntime } from '../../src/query/live/includes-runtime.js' import { CollectionImpl } from '../../src/collection/index.js' @@ -873,7 +871,6 @@ function createAlphaRenamedJoinQuery( ), join: [ { - runtime: joinRuntime, type: `inner`, from: new CollectionRef( postsCollection as unknown as ConstructorParameters< @@ -916,7 +913,6 @@ function createAlphaRenamedImplicitJoinQuery( ), join: [ { - runtime: joinRuntime, type: `inner`, from: new CollectionRef( postsCollection as unknown as ConstructorParameters< @@ -1283,9 +1279,9 @@ const structuredQueries: Array<[string, () => QueryIR]> = [ new Query() .from({ user: usersCollection }) .join( - leftJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.userId), - ), + { post: postsCollection }, + ({ user, post }) => eq(user.id, post.userId), + `left`, ) .where(({ post }) => eq(post.published, true)) .select(({ user, post }) => ({ @@ -1305,14 +1301,13 @@ const structuredQueries: Array<[string, () => QueryIR]> = [ .where(({ post }) => gt(post.views, 100)), }) .join( - innerJoin( - { - activeUser: new Query() - .from({ user: usersCollection }) - .where(({ user }) => eq(user.status, `active`)), - }, - ({ post, activeUser }) => eq(post.userId, activeUser.id), - ), + { + activeUser: new Query() + .from({ user: usersCollection }) + .where(({ user }) => eq(user.status, `active`)), + }, + ({ post, activeUser }) => eq(post.userId, activeUser.id), + `inner`, ), ), ], diff --git a/packages/db/tests/query/join-result-key-oracle.property.test.ts b/packages/db/tests/query/join-result-key-oracle.property.test.ts index 1f6dca0e06..cd844ef9d8 100644 --- a/packages/db/tests/query/join-result-key-oracle.property.test.ts +++ b/packages/db/tests/query/join-result-key-oracle.property.test.ts @@ -42,7 +42,7 @@ */ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, it } from 'vitest' -import { createLiveQueryCollection, eq, joinOf } from '../../src/query/index.js' +import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { oraclePropertyOptions, @@ -224,7 +224,7 @@ async function runHistory(history: History): Promise { const live = createLiveQueryCollection((q) => q .from({ l: left }) - .join(joinOf(history.join, { r: right }, ({ l, r }) => eq(l.g, r.g))) + .join({ r: right }, ({ l, r }) => eq(l.g, r.g), history.join) .select(({ l, r }) => ({ l: l.id, r: r.id })), ) const check = (checkpoint: string) => { diff --git a/packages/db/tests/query/join-subquery.test-d.ts b/packages/db/tests/query/join-subquery.test-d.ts index 92fc49f829..070584a7a9 100644 --- a/packages/db/tests/query/join-subquery.test-d.ts +++ b/packages/db/tests/query/join-subquery.test-d.ts @@ -1,11 +1,5 @@ import { describe, expectTypeOf, test } from 'vitest' -import { - createLiveQueryCollection, - eq, - gt, - innerJoin, - leftJoin, -} from '../../src/query/index.js' +import { createLiveQueryCollection, eq, gt } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -106,9 +100,9 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: project1Issues }) .join( - innerJoin({ user: usersCollection }, ({ issue, user }) => - eq(issue.userId, user.id), - ), + { user: usersCollection }, + ({ issue, user }) => eq(issue.userId, user.id), + `inner`, ) .select(({ issue, user }) => ({ issue_title: issue.title, @@ -145,9 +139,9 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: issuesCollection }) .join( - leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ), + { activeUser: activeUsers }, + ({ issue, activeUser }) => eq(issue.userId, activeUser.id), + `left`, ) }, }) @@ -181,11 +175,10 @@ describe(`Join Subquery Types`, () => { return q .from({ longIssue: longIssues }) .join( - innerJoin( - { activeUser: activeUsers }, - ({ longIssue, activeUser }) => - eq(longIssue.userId, activeUser.id), - ), + { activeUser: activeUsers }, + ({ longIssue, activeUser }) => + eq(longIssue.userId, activeUser.id), + `inner`, ) .select(({ longIssue, activeUser }) => ({ issue_title: longIssue.title, @@ -223,9 +216,9 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: issuesCollection }) .join( - innerJoin({ engUser: engineeringUsers }, ({ issue, engUser }) => - eq(issue.userId, engUser.id), - ), + { engUser: engineeringUsers }, + ({ issue, engUser }) => eq(issue.userId, engUser.id), + `inner`, ) .select(({ issue, engUser }) => ({ issue_title: issue.title, @@ -259,9 +252,9 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: issuesCollection }) .join( - leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ), + { activeUser: activeUsers }, + ({ issue, activeUser }) => eq(issue.userId, activeUser.id), + `left`, ) }, }) @@ -308,9 +301,9 @@ describe(`Join Subquery Types`, () => { return q .from({ task: transformedIssues }) .join( - innerJoin({ profile: userProfiles }, ({ task, profile }) => - eq(task.assigneeId, profile.profileId), - ), + { profile: userProfiles }, + ({ task, profile }) => eq(task.assigneeId, profile.profileId), + `inner`, ) .select(({ task, profile }) => ({ id: task.taskId, @@ -354,9 +347,9 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: filteredIssues }) .join( - innerJoin({ user: usersCollection }, ({ issue, user }) => - eq(issue.userId, user.id), - ), + { user: usersCollection }, + ({ issue, user }) => eq(issue.userId, user.id), + `inner`, ) .select(({ issue, user }) => ({ // Should have access to all original Issue properties @@ -402,9 +395,9 @@ describe(`Join Subquery Types`, () => { return q .from({ issue: issuesCollection }) .join( - leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ), + { activeUser: activeUsers }, + ({ issue, activeUser }) => eq(issue.userId, activeUser.id), + `left`, ) .select(({ issue, activeUser }) => ({ issue_title: issue.title, diff --git a/packages/db/tests/query/join-subquery.test.ts b/packages/db/tests/query/join-subquery.test.ts index f5791185c0..a0f4b6e779 100644 --- a/packages/db/tests/query/join-subquery.test.ts +++ b/packages/db/tests/query/join-subquery.test.ts @@ -1,13 +1,5 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { - createLiveQueryCollection, - eq, - gt, - innerJoin, - joinOf, - leftJoin, - rightJoin, -} from '../../src/query/index.js' +import { createLiveQueryCollection, eq, gt } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' @@ -232,9 +224,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: project1Issues }) .join( - innerJoin({ user: usersCollection }, ({ issue, user }) => - eq(issue.userId, user.id), - ), + { user: usersCollection }, + ({ issue, user }) => eq(issue.userId, user.id), + `inner`, ) .select(({ issue, user }) => ({ issue_title: issue.title, @@ -271,9 +263,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) .join( - leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ), + { activeUser: activeUsers }, + ({ issue, activeUser }) => eq(issue.userId, activeUser.id), + `left`, ) .select(({ issue, activeUser }) => ({ issue_title: issue.title, @@ -319,11 +311,10 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ longIssue: longIssues }) .join( - innerJoin( - { activeUser: activeUsers }, - ({ longIssue, activeUser }) => - eq(longIssue.userId, activeUser.id), - ), + { activeUser: activeUsers }, + ({ longIssue, activeUser }) => + eq(longIssue.userId, activeUser.id), + `inner`, ) .select(({ longIssue, activeUser }) => ({ issue_title: longIssue.title, @@ -381,9 +372,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) .join( - innerJoin({ engUser: engineeringUsers }, ({ issue, engUser }) => - eq(issue.userId, engUser.id), - ), + { engUser: engineeringUsers }, + ({ issue, engUser }) => eq(issue.userId, engUser.id), + `inner`, ) .select(({ issue, engUser }) => ({ issue_title: issue.title, @@ -424,9 +415,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) .join( - joinOf(joinType, { user: activeUsers }, ({ issue, user }) => - eq(issue.userId, user.id), - ), + { user: activeUsers }, + ({ issue, user }) => eq(issue.userId, user.id), + joinType, ) .where(({ user }) => eq(user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) @@ -435,11 +426,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) .join( - joinOf( - joinType, - { user: usersCollection }, - ({ issue, user }) => eq(issue.userId, user.id), - ), + { user: usersCollection }, + ({ issue, user }) => eq(issue.userId, user.id), + joinType, ) .where(({ user }) => eq(user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) @@ -460,10 +449,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .join( - innerJoin({ user: activeUsers }, ({ issue, user }) => - eq(issue.userId, user.id), - ), + .innerJoin({ user: activeUsers }, ({ issue, user }) => + eq(issue.userId, user.id), ) .where(({ user }) => eq(user.name, `Charlie`)) .select(({ issue }) => ({ id: issue.id })) @@ -483,10 +470,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) - .join( - innerJoin({ member: activeUsers }, ({ issue, member }) => - eq(issue.userId, member.id), - ), + .innerJoin({ member: activeUsers }, ({ issue, member }) => + eq(issue.userId, member.id), ) .where(({ member }) => eq(member.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) @@ -502,19 +487,14 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { query: (q) => { const usersWithProfiles = q .from({ user: usersCollection }) - .join( - innerJoin( - { profile: profilesCollection }, - ({ user, profile }) => eq(user.id, profile.userId), - ), + .innerJoin({ profile: profilesCollection }, ({ user, profile }) => + eq(user.id, profile.userId), ) return q .from({ issue: issuesCollection }) - .join( - innerJoin({ member: usersWithProfiles }, ({ issue, member }) => - eq(issue.userId, member.user.id), - ), + .innerJoin({ member: usersWithProfiles }, ({ issue, member }) => + eq(issue.userId, member.user.id), ) .where(({ member }) => eq(member.user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) @@ -530,20 +510,15 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { query: (q) => { const usersWithProfiles = q .from({ user: usersCollection }) - .join( - innerJoin( - { profile: profilesCollection }, - ({ user, profile }) => eq(user.id, profile.userId), - ), + .innerJoin({ profile: profilesCollection }, ({ user, profile }) => + eq(user.id, profile.userId), ) .select(({ user }) => user) return q .from({ issue: issuesCollection }) - .join( - innerJoin({ member: usersWithProfiles }, ({ issue, member }) => - eq(issue.userId, member.id), - ), + .innerJoin({ member: usersWithProfiles }, ({ issue, member }) => + eq(issue.userId, member.id), ) .where(({ member }) => eq(member.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) @@ -565,9 +540,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) .join( - leftJoin({ activeUser: activeUsers }, ({ issue, activeUser }) => - eq(issue.userId, activeUser.id), - ), + { activeUser: activeUsers }, + ({ issue, activeUser }) => eq(issue.userId, activeUser.id), + `left`, ) .select(({ issue, activeUser }) => ({ issue_title: issue.title, @@ -603,14 +578,13 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ product: productsCollection }) .join( - leftJoin( - { - tried: q - .from({ tried: trialsCollection }) - .where(({ tried }) => eq(tried.userId, 1)), - }, - ({ tried, product }) => eq(tried.productId, product.id), - ), + { + tried: q + .from({ tried: trialsCollection }) + .where(({ tried }) => eq(tried.userId, 1)), + }, + ({ tried, product }) => eq(tried.productId, product.id), + `left`, ) .where(({ product }) => eq(product.id, 1)) .select(({ product, tried }) => ({ @@ -642,16 +616,15 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) .join( - leftJoin( - { - users: q - .from({ user: usersCollection }) - .where(({ user }) => eq(user.status, `active`)) - .orderBy(({ user }) => user.name, `asc`) - .limit(1), - }, - ({ issue, users }) => eq(issue.userId, users.id), - ), + { + users: q + .from({ user: usersCollection }) + .where(({ user }) => eq(user.status, `active`)) + .orderBy(({ user }) => user.name, `asc`) + .limit(1), + }, + ({ issue, users }) => eq(issue.userId, users.id), + `left`, ) .orderBy(({ issue }) => issue.id, `desc`) .limit(1) @@ -693,9 +666,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { .limit(1), }) .join( - rightJoin({ issue: issuesCollection }, ({ issue, users }) => - eq(issue.userId, users.id), - ), + { issue: issuesCollection }, + ({ issue, users }) => eq(issue.userId, users.id), + `right`, ) .orderBy(({ issue }) => issue.id, `desc`) .limit(1) @@ -755,9 +728,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ task: transformedIssues }) .join( - innerJoin({ profile: userProfiles }, ({ task, profile }) => - eq(task.assigneeId, profile.profileId), - ), + { profile: userProfiles }, + ({ task, profile }) => eq(task.assigneeId, profile.profileId), + `inner`, ) .select(({ task, profile }) => ({ id: task.taskId, @@ -833,10 +806,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ user: activeUsers }) - .join( - innerJoin({ issue: issuesCollection }, ({ user, issue }) => - eq(user.id, issue.userId), - ), + .innerJoin({ issue: issuesCollection }, ({ user, issue }) => + eq(user.id, issue.userId), ) .where(({ user }) => eq(user.name, `Bob`)) .select(({ issue }) => ({ id: issue.id })) @@ -872,10 +843,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ row: allIssues }) - .join( - innerJoin({ user: usersCollection }, ({ row, user }) => - eq(row.userId, user.id), - ), + .innerJoin({ user: usersCollection }, ({ row, user }) => + eq(row.userId, user.id), ) .where(({ row }) => eq(row.status, `open`)) .select(({ row }) => ({ id: row.id })) @@ -906,10 +875,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { const activeUsersWithProfiles = q .from({ user: usersCollection }) .join( - innerJoin( - { profile: profilesCollection }, - ({ user, profile }) => eq(user.id, profile.userId), - ), + { profile: profilesCollection }, + ({ user, profile }) => eq(user.id, profile.userId), + `inner`, ) .where(({ user }) => eq(user.status, `active`)) .select(({ user, profile }) => ({ @@ -926,11 +894,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ activeUser: activeUsersWithProfiles }) .join( - innerJoin( - { issue: issuesCollection }, - ({ activeUser, issue }) => - eq(issue.userId, activeUser.userId), - ), + { issue: issuesCollection }, + ({ activeUser, issue }) => eq(issue.userId, activeUser.userId), + `inner`, ) .select(({ activeUser, issue }) => ({ issue_title: issue.title, @@ -992,10 +958,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { const usersWithProfiles = q .from({ user: usersCollection }) .join( - innerJoin( - { profile: profilesCollection }, - ({ user, profile }) => eq(user.id, profile.userId), - ), + { profile: profilesCollection }, + ({ user, profile }) => eq(user.id, profile.userId), + `inner`, ) .where(({ user }) => eq(user.status, `active`)) .select(({ user, profile }) => ({ @@ -1010,9 +975,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) .join( - leftJoin({ author: usersWithProfiles }, ({ issue, author }) => - eq(issue.userId, author.userId), - ), + { author: usersWithProfiles }, + ({ issue, author }) => eq(issue.userId, author.userId), + `left`, ) .select(({ issue, author }) => ({ issue_id: issue.id, @@ -1047,10 +1012,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { const usersWithProfiles = q .from({ user: usersCollection }) .join( - innerJoin( - { profile: profilesCollection }, - ({ user, profile }) => eq(user.id, profile.userId), - ), + { profile: profilesCollection }, + ({ user, profile }) => eq(user.id, profile.userId), + `inner`, ) .select(({ user, profile }) => ({ userId: user.id, @@ -1075,10 +1039,9 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { return q .from({ issue: issuesCollection }) .join( - innerJoin( - { author: activeUsersWithProfiles }, - ({ issue, author }) => eq(issue.userId, author.id), - ), + { author: activeUsersWithProfiles }, + ({ issue, author }) => eq(issue.userId, author.id), + `inner`, ) .select(({ issue, author }) => ({ issue_title: issue.title, @@ -1133,10 +1096,8 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void { const right = shared ? left : activeUsers() let query = q .from({ leftUser: left }) - .join( - innerJoin({ rightUser: right }, ({ leftUser, rightUser }) => - eq(leftUser.id, rightUser.id), - ), + .innerJoin({ rightUser: right }, ({ leftUser, rightUser }) => + eq(leftUser.id, rightUser.id), ) if (filterRight) { @@ -1212,19 +1173,15 @@ describe(`Lazy join: subquery whose join key resolves to an indexed collection`, query: (q) => { const teamByMember = q .from({ member: members }) - .join( - leftJoin({ team: teams }, ({ team, member }) => - eq(team.id, member.teamId), - ), + .leftJoin({ team: teams }, ({ team, member }) => + eq(team.id, member.teamId), ) .select(({ team }) => ({ teamId: team.id })) return q .from({ m: members }) - .join( - leftJoin({ memberTeam: teamByMember }, ({ m, memberTeam }) => - eq(memberTeam.teamId, m.teamId), - ), + .leftJoin({ memberTeam: teamByMember }, ({ m, memberTeam }) => + eq(memberTeam.teamId, m.teamId), ) .select(({ m }) => ({ id: m.id })) }, @@ -1282,10 +1239,8 @@ describe(`Lazy join index availability`, () => { const live = createLiveQueryCollection((q) => q .from({ team: teams }) - .join( - leftJoin({ member: members }, ({ team, member }) => - eq(team.id, member.teamId), - ), + .leftJoin({ member: members }, ({ team, member }) => + eq(team.id, member.teamId), ) .select(({ team, member }) => ({ id: team.id, @@ -1340,10 +1295,8 @@ describe(`Lazy join index availability`, () => { const live = createLiveQueryCollection((q) => q .from({ team: teams }) - .join( - leftJoin({ member: members }, ({ team, member }) => - eq(team.id, member.teamId), - ), + .leftJoin({ member: members }, ({ team, member }) => + eq(team.id, member.teamId), ) .select(({ team, member }) => ({ id: team.id, diff --git a/packages/db/tests/query/join.test-d.ts b/packages/db/tests/query/join.test-d.ts index 4e1fa8f63a..03c8f49899 100644 --- a/packages/db/tests/query/join.test-d.ts +++ b/packages/db/tests/query/join.test-d.ts @@ -1,14 +1,7 @@ import { describe, expectTypeOf, test } from 'vitest' import { z } from 'zod' import { type } from 'arktype' -import { - createLiveQueryCollection, - eq, - fullJoin, - innerJoin, - leftJoin, - rightJoin, -} from '../../src/query/index.js' +import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, omitVirtualProps } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -60,9 +53,9 @@ describe(`Join Types - Type Safety`, () => { q .from({ user: usersCollection }) .join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `inner`, ), }) @@ -88,9 +81,9 @@ describe(`Join Types - Type Safety`, () => { q .from({ user: usersCollection }) .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `left`, ), }) @@ -116,9 +109,9 @@ describe(`Join Types - Type Safety`, () => { q .from({ user: usersCollection }) .join( - rightJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `right`, ), }) @@ -144,9 +137,9 @@ describe(`Join Types - Type Safety`, () => { q .from({ user: usersCollection }) .join( - fullJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `full`, ), }) @@ -187,14 +180,14 @@ describe(`Join Types - Type Safety`, () => { q .from({ user: usersCollection }) .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `left`, ) .join( - rightJoin({ project: projectsCollection }, ({ user, project }) => - eq(user.id, project.user_id), - ), + { project: projectsCollection }, + ({ user, project }) => eq(user.id, project.user_id), + `right`, ), }) @@ -224,9 +217,9 @@ describe(`Join Types - Type Safety`, () => { q .from({ user: usersCollection }) .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `left`, ) .select(({ user, dept }) => ({ userName: user.name, @@ -259,10 +252,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ), }) @@ -287,10 +278,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - rightJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .rightJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ), }) @@ -315,10 +304,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ), }) @@ -343,10 +330,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - fullJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .fullJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ), }) @@ -386,15 +371,11 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) - .join( - rightJoin({ project: projectsCollection }, ({ user, project }) => - eq(user.id, project.user_id), - ), + .rightJoin({ project: projectsCollection }, ({ user, project }) => + eq(user.id, project.user_id), ), }) @@ -423,10 +404,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => ({ userName: user.name, @@ -457,10 +436,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => ({ userName: user.name, @@ -505,15 +482,13 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .join( - innerJoin({ project: projectsCollection }, ({ dept, project }) => - eq(dept.id, project.department_id), - ), + { project: projectsCollection }, + ({ dept, project }) => eq(dept.id, project.department_id), + `inner`, ), }) @@ -568,10 +543,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .join( - innerJoin({ user: userCollection }, ({ event, user }) => - eq(event.user_id, user.id), - ), + .innerJoin({ user: userCollection }, ({ event, user }) => + eq(event.user_id, user.id), ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -584,10 +557,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .join( - innerJoin({ user: userCollection }, ({ event, user }) => - eq(user.id, event.user_id), - ), + .innerJoin({ user: userCollection }, ({ event, user }) => + eq(user.id, event.user_id), ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -659,10 +630,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .join( - innerJoin({ user: userCollection }, ({ event, user }) => - eq(event.user_id, user.id), - ), + .innerJoin({ user: userCollection }, ({ event, user }) => + eq(event.user_id, user.id), ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -675,10 +644,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .join( - innerJoin({ user: userCollection }, ({ event, user }) => - eq(user.id, event.user_id), - ), + .innerJoin({ user: userCollection }, ({ event, user }) => + eq(user.id, event.user_id), ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -750,10 +717,8 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ post: postCollection }) - .join( - leftJoin({ user: userCollection }, ({ post, user }) => - eq(post.author_id, user.id), - ), + .leftJoin({ user: userCollection }, ({ post, user }) => + eq(post.author_id, user.id), ) .select(({ post, user }) => ({ postTitle: post.title, @@ -782,10 +747,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => ({ userName: user.name, @@ -819,10 +782,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join( - rightJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .rightJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => ({ // user is the nullable side in a right join @@ -851,10 +812,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join( - fullJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .fullJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => ({ userName: user.name, @@ -883,10 +842,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => ({ userName: user.name, @@ -919,10 +876,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => ({ userName: user.name, @@ -954,10 +909,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join( - rightJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .rightJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => ({ // user is the nullable side in a right join @@ -984,10 +937,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join( - fullJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .fullJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => ({ userName: user.name, @@ -1014,10 +965,8 @@ describe(`Declarative select refs should not use union with undefined for nullab query: (q) => q .from({ user: usersCollection }) - .join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => ({ userName: user.name, @@ -1082,10 +1031,8 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ event: eventCollection }) - .join( - innerJoin({ user: userCollection }, ({ event, user }) => - eq(event.user_id, user.id), - ), + .innerJoin({ user: userCollection }, ({ event, user }) => + eq(event.user_id, user.id), ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -1098,10 +1045,8 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ event: eventCollection }) - .join( - innerJoin({ user: userCollection }, ({ event, user }) => - eq(user.id, event.user_id), - ), + .innerJoin({ user: userCollection }, ({ event, user }) => + eq(user.id, event.user_id), ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -1173,10 +1118,8 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ post: postCollection }) - .join( - leftJoin({ user: userCollection }, ({ post, user }) => - eq(post.author_id, user.id), - ), + .leftJoin({ user: userCollection }, ({ post, user }) => + eq(post.author_id, user.id), ) .select(({ post, user }) => ({ postTitle: post.title, @@ -1241,10 +1184,8 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ post: postCollection }) - .join( - innerJoin({ user: userCollection }, ({ post, user }) => - eq(post.user_id, user.id), - ), + .innerJoin({ user: userCollection }, ({ post, user }) => + eq(post.user_id, user.id), ) .select(({ post, user }) => ({ postTitle: post.title, diff --git a/packages/db/tests/query/join.test.ts b/packages/db/tests/query/join.test.ts index 9bf94c728d..c08f56ac38 100644 --- a/packages/db/tests/query/join.test.ts +++ b/packages/db/tests/query/join.test.ts @@ -4,18 +4,13 @@ import { concat, createLiveQueryCollection, eq, - fullJoin, gt, inArray, - innerJoin, isNull, isUndefined, - joinOf, - leftJoin, lt, not, or, - rightJoin, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { @@ -125,11 +120,9 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { q .from({ user: usersCollection }) .join( - joinOf( - joinType, - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + joinType, ) .select(({ user, dept }) => ({ user_name: user.name, @@ -215,11 +208,9 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { q .from({ user: usersCollection }) .join( - joinOf( - joinType, - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + joinType, ), }) @@ -310,11 +301,9 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { q .from({ user: usersCollection }) .join( - joinOf( - joinType, - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + joinType, ) .select(({ user, dept }) => ({ user_name: user.name, @@ -355,11 +344,9 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { q .from({ user: usersCollection }) .join( - joinOf( - joinType, - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + joinType, ) .select(({ user, dept }) => ({ user_name: user.name, @@ -396,11 +383,9 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { q .from({ user: usersCollection }) .join( - joinOf( - joinType, - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + joinType, ) .select(({ user, dept }) => ({ user_name: user.name, @@ -447,11 +432,9 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { q .from({ user: usersCollection }) .join( - joinOf( - joinType, - { dept: departmentsCollection }, - ({ user, dept }) => eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + joinType, ) .select(({ user, dept }) => ({ user_name: user.name, @@ -551,11 +534,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .join( - leftJoin( - { member: teamMembersCollection }, - ({ team, member }) => eq(team.id, member.team_id), - ), + .leftJoin({ member: teamMembersCollection }, ({ team, member }) => + eq(team.id, member.team_id), ) .where(({ member }) => eq(member.user_id, 100)) .select(({ team, member }) => ({ @@ -573,11 +553,9 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .join( - rightJoin( - { member: teamMembersCollection }, - ({ team, member }) => eq(team.id, member.team_id), - ), + .rightJoin( + { member: teamMembersCollection }, + ({ team, member }) => eq(team.id, member.team_id), ) .where(({ team }) => eq(team.active, true)) .select(({ team, member }) => ({ @@ -595,11 +573,8 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .join( - fullJoin( - { member: teamMembersCollection }, - ({ team, member }) => eq(team.id, member.team_id), - ), + .fullJoin({ member: teamMembersCollection }, ({ team, member }) => + eq(team.id, member.team_id), ) .where(({ member }) => eq(member.role, `admin`)) .select(({ team, member }) => ({ @@ -617,11 +592,9 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { query: (q) => q .from({ team: teamsCollection }) - .join( - innerJoin( - { member: teamMembersCollection }, - ({ team, member }) => eq(team.id, member.team_id), - ), + .innerJoin( + { member: teamMembersCollection }, + ({ team, member }) => eq(team.id, member.team_id), ) .where(({ member }) => eq(member.user_id, 100)) .select(({ team, member }) => ({ @@ -761,16 +734,14 @@ function testJoinType(joinType: JoinType, autoIndex: `off` | `eager`) { q .from({ company: companiesCollection }) .join( - joinOf( - joinType, - { project: projectsCollection }, - ({ company, project }) => eq(project.company_id, company.id), - ), + { project: projectsCollection }, + ({ company, project }) => eq(project.company_id, company.id), + joinType, ) .join( - joinOf(joinType, { task: tasksCollection }, ({ task, project }) => - eq(task.project_id, project.id), - ), + { task: tasksCollection }, + ({ task, project }) => eq(task.project_id, project.id), + joinType, ) .select(({ company, project, task }) => ({ company_name: company.name, @@ -852,9 +823,9 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ user: usersCollection }) .join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `inner`, ) .select(({ user, dept }) => ({ user_name: user.name, @@ -927,9 +898,9 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ user: emptyUsers }) .join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `inner`, ) .select(({ user, dept }) => ({ user_name: user.name, @@ -966,9 +937,9 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ user: usersCollection }) .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `left`, ) .select(({ user, dept }) => ({ user_id: user.id, @@ -1002,12 +973,13 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ user: usersCollection }) .join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + { dept: departmentsCollection }, + ({ user, dept }) => eq( concat(`dept`, user.department_id), concat(`dept`, dept.id), ), - ), + `inner`, ) .select(({ user, dept }) => ({ user_name: user.name, @@ -1059,10 +1031,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ left: leftCollection }) - .join( - innerJoin({ right: rightCollection }, ({ left, right }) => - eq(left.joinedAt, right.joinedAt), - ), + .innerJoin({ right: rightCollection }, ({ left, right }) => + eq(left.joinedAt, right.joinedAt), ) .select(({ left, right }) => ({ leftId: left.id, @@ -1113,10 +1083,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ left: leftCollection }) - .join( - innerJoin({ right: rightCollection }, ({ left, right }) => - eq(left.joinedAt, right.joinedAt), - ), + .innerJoin({ right: rightCollection }, ({ left, right }) => + eq(left.joinedAt, right.joinedAt), ) .select(({ left, right }) => ({ leftId: left.id, @@ -1221,16 +1189,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ player: playersCollection }) - .join( - innerJoin({ client: clientsCollection }, ({ client, player }) => - eq(client.player, player.name), - ), + .innerJoin({ client: clientsCollection }, ({ client, player }) => + eq(client.player, player.name), ) - .join( - innerJoin( - { balance: balancesCollection }, - ({ balance, client }) => eq(balance.client, client.name), - ), + .innerJoin({ balance: balancesCollection }, ({ balance, client }) => + eq(balance.client, client.name), ) .select(({ player, client, balance }) => ({ player_name: player.name, @@ -1375,10 +1338,9 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ users: selfJoinUsersCollection }) .join( - innerJoin( - { parentUsers: selfJoinUsersCollection }, - ({ users, parentUsers }) => eq(users.parentId, parentUsers.id), - ), + { parentUsers: selfJoinUsersCollection }, + ({ users, parentUsers }) => eq(users.parentId, parentUsers.id), + `inner`, ) .select(({ users, parentUsers }) => ({ user_id: users.id, @@ -1448,10 +1410,9 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ users: bidirectionalUsersCollection }) .join( - innerJoin( - { parentUsers: bidirectionalUsersCollection }, - ({ users, parentUsers }) => eq(users.parentId, parentUsers.id), - ), + { parentUsers: bidirectionalUsersCollection }, + ({ users, parentUsers }) => eq(users.parentId, parentUsers.id), + `inner`, ) .select(({ users, parentUsers }) => ({ user_name: users.name, @@ -1471,10 +1432,9 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ users: bidirectionalUsersCollection }) .join( - innerJoin( - { parentUsers: bidirectionalUsersCollection }, - ({ users, parentUsers }) => eq(parentUsers.id, users.parentId), - ), + { parentUsers: bidirectionalUsersCollection }, + ({ users, parentUsers }) => eq(parentUsers.id, users.parentId), + `inner`, ) .select(({ users, parentUsers }) => ({ user_name: users.name, @@ -1515,13 +1475,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { createLiveQueryCollection({ startSync: true, query: (q) => - q - .from({ user: usersCollection }) - .join( - innerJoin({ dept: departmentsCollection }, ({ user }) => - eq(user.id, user.department_id), - ), - ), + q.from({ user: usersCollection }).join( + { dept: departmentsCollection }, + ({ user }) => eq(user.id, user.department_id), // Both refer to 'user' table + `inner`, + ), }) }).toThrow( `Invalid join condition: both expressions refer to the same source "user"`, @@ -1543,9 +1501,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { createLiveQueryCollection({ startSync: true, query: (q) => - q - .from({ user: usersCollection }) - .join(innerJoin({ dept: departmentsCollection }, () => eq(1, 2))), + q.from({ user: usersCollection }).join( + { dept: departmentsCollection }, + () => eq(1, 2), // Constants, no table references + `inner`, + ), }) }).toThrow( `Invalid join condition: expressions must reference source aliases`, @@ -1572,14 +1532,14 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ user: usersCollection }) .join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(dept.id, user.department_id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(dept.id, user.department_id), + `inner`, ) .join( - innerJoin({ dept2: departmentsCollection2 }, ({ user, dept }) => - eq(dept.id, user.department_id), - ), + { dept2: departmentsCollection2 }, + ({ user, dept }) => eq(dept.id, user.department_id), + `inner`, ), }) }).toThrow( @@ -1602,13 +1562,11 @@ function createJoinTests(autoIndex: `off` | `eager`): void { createLiveQueryCollection({ startSync: true, query: (q) => - q - .from({ user: usersCollection }) - .join( - innerJoin({ dept: departmentsCollection }, ({ user }) => - eq(user.id, user.department_id), - ), - ), + q.from({ user: usersCollection }).join( + { dept: departmentsCollection }, + ({ user }) => eq(user.id, user.department_id), // Both refer to 'user' table + `inner`, + ), }) }).toThrow( `Invalid join condition: both expressions refer to the same source "user"`, @@ -1630,11 +1588,9 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin( - { special: specialUsersCollection }, - ({ user, special }) => eq(user.id, special.id), - ), + .leftJoin( + { special: specialUsersCollection }, + ({ user, special }) => eq(user.id, special.id), ) .where(({ special }) => isUndefined(special)), }) @@ -1703,9 +1659,9 @@ function createJoinTests(autoIndex: `off` | `eager`): void { eq(event.id, `3770a4a6-3260-4566-9f79-f50864ebdd46`), ) .join( - leftJoin({ parent: eventCollection }, ({ event, parent }) => - eq(parent.id, event.parent_id), - ), + { parent: eventCollection }, + ({ event, parent }) => eq(parent.id, event.parent_id), + `left`, ) .select(({ event, parent }) => ({ id: event.id, @@ -1768,9 +1724,9 @@ function createJoinTests(autoIndex: `off` | `eager`): void { .from({ employee: peopleCollection }) .where(({ employee }) => gt(employee.age, 30)) .join( - leftJoin({ manager: peopleCollection }, ({ employee, manager }) => - eq(employee.manager_id, manager.id), - ), + { manager: peopleCollection }, + ({ employee, manager }) => eq(employee.manager_id, manager.id), + `left`, ) .where(({ manager }) => or(isNull(manager.id), gt(manager.age, 35))) .select(({ employee, manager }) => ({ @@ -1857,24 +1813,22 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ main: collection1 }) .join( - leftJoin( - { - join1: q - .from({ join1: collection2 }) - .where(({ join1 }) => not(gt(join1.other, 20))), - }, - ({ main, join1 }) => eq(main.value, join1.value), - ), + { + join1: q + .from({ join1: collection2 }) + .where(({ join1 }) => not(gt(join1.other, 20))), + }, + ({ main, join1 }) => eq(main.value, join1.value), + `left`, ) .join( - leftJoin( - { - join2: q - .from({ join2: collection2 }) - .where(({ join2 }) => not(lt(join2.other, 20))), - }, - ({ main, join2 }) => eq(main.value, join2.value), - ), + { + join2: q + .from({ join2: collection2 }) + .where(({ join2 }) => not(lt(join2.other, 20))), + }, + ({ main, join2 }) => eq(main.value, join2.value), + `left`, ), }) @@ -1945,9 +1899,7 @@ function createJoinTests(autoIndex: `off` | `eager`): void { query: (q) => q .from({ l: leftCollection }) - .join( - leftJoin({ r: rightCollection }, ({ l, r }) => eq(l.rightId, r.id)), - ) + .leftJoin({ r: rightCollection }, ({ l, r }) => eq(l.rightId, r.id)) .where(({ r }) => isUndefined(r.payload)) .select(({ l, r }) => ({ leftId: l.id, right: r })), }) @@ -2042,14 +1994,14 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ player: playersCollection }) .join( - leftJoin({ client: clientsCollection }, ({ client, player }) => - eq(client.player, player.name), - ), + { client: clientsCollection }, + ({ client, player }) => eq(client.player, player.name), + `left`, ) .join( - leftJoin({ balance: balancesCollection }, ({ balance, client }) => - eq(balance.client, client.name), - ), + { balance: balancesCollection }, + ({ balance, client }) => eq(balance.client, client.name), + `left`, ) .select(({ player, client, balance }) => ({ player_name: player.name, @@ -2140,10 +2092,8 @@ function createJoinTests(autoIndex: `off` | `eager`): void { q .from({ task: taskCollection }) .where(({ task }) => inArray(task.id, [1])) - .join( - innerJoin({ project: projectCollection }, ({ task, project }) => - eq(task.project_id, project.id), - ), + .innerJoin({ project: projectCollection }, ({ task, project }) => + eq(task.project_id, project.id), ) .select(({ task, project }) => ({ task, diff --git a/packages/db/tests/query/live-query-collection.test.ts b/packages/db/tests/query/live-query-collection.test.ts index 4b7790a61c..f8c22204f9 100644 --- a/packages/db/tests/query/live-query-collection.test.ts +++ b/packages/db/tests/query/live-query-collection.test.ts @@ -6,8 +6,6 @@ import { createLiveQueryCollection, eq, ilike, - innerJoin, - leftJoin, liveQueryCollectionOptions, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' @@ -917,9 +915,10 @@ describe(`createLiveQueryCollection`, () => { q .from({ thread: sourceLQ }) .join( - innerJoin({ label: labelsByEmail }, ({ thread, label }: any) => + { label: labelsByEmail }, + ({ thread, label }: any) => eq(thread.last_email_id, label.email_id), - ), + `inner`, ) .orderBy(({ thread }: any) => thread.last_sent_at, { direction: `desc`, @@ -1108,17 +1107,13 @@ describe(`createLiveQueryCollection`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ player: playerCollection }) - .join( - leftJoin( - { challenge1: challenge1Collection }, - ({ player, challenge1 }) => eq(player.id, challenge1.id), - ), + .leftJoin( + { challenge1: challenge1Collection }, + ({ player, challenge1 }) => eq(player.id, challenge1.id), ) - .join( - leftJoin( - { challenge2: challenge2Collection }, - ({ player, challenge2 }) => eq(player.id, challenge2.id), - ), + .leftJoin( + { challenge2: challenge2Collection }, + ({ player, challenge2 }) => eq(player.id, challenge2.id), ), ) @@ -1976,10 +1971,8 @@ describe(`createLiveQueryCollection`, () => { const live = createLiveQueryCollection((q) => q .from({ user: users }) - .join( - leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ), + .leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), ) .select(({ user, issue }) => ({ id: user.id, @@ -2024,10 +2017,8 @@ describe(`createLiveQueryCollection`, () => { const live = createLiveQueryCollection((q) => q .from({ user: users }) - .join( - leftJoin({ issue: issues }, ({ user, issue }) => - eq(user.id, issue.userId), - ), + .leftJoin({ issue: issues }, ({ user, issue }) => + eq(user.id, issue.userId), ) .select(({ user, issue }) => ({ id: user.id, @@ -2125,10 +2116,8 @@ describe(`createLiveQueryCollection`, () => { const live = createLiveQueryCollection((q) => q .from({ parent: parents }) - .join( - leftJoin({ child: children }, ({ parent, child }) => - eq(parent.id, child.parentId), - ), + .leftJoin({ child: children }, ({ parent, child }) => + eq(parent.id, child.parentId), ) .orderBy(({ parent }) => parent.rank, `asc`) .limit(1) @@ -3988,9 +3977,9 @@ describe(`createLiveQueryCollection`, () => { q .from({ user: usersCollection }) .join( - innerJoin({ post: posts }, ({ user, post }) => - eq(user.id, post.authorId), - ), + { post: posts }, + ({ user, post }) => eq(user.id, post.authorId), + `inner`, ) .where(({ user, post }) => and(eq(user.active, true), eq(post.published, true)), @@ -4497,11 +4486,7 @@ describe(`createLiveQueryCollection`, () => { query: (q) => q .from({ base }) - .join( - leftJoin({ related }, ({ base: b, related: r }) => - eq(b.id, r.id), - ), - ) + .join({ related }, ({ base: b, related: r }) => eq(b.id, r.id)) .select(({ base: b, related: r }) => ({ id: b.id, name: b.name, @@ -4542,11 +4527,7 @@ describe(`createLiveQueryCollection`, () => { query: (q) => q .from({ comments }) - .join( - leftJoin({ users }, ({ comments: c, users: u }) => - eq(c.userId, u.id), - ), - ) + .join({ users }, ({ comments: c, users: u }) => eq(c.userId, u.id)) .select(({ comments: c, users: u }) => ({ id: c.id, userId: coalesce(u.id, c.userId), @@ -4939,10 +4920,8 @@ describe(`createLiveQueryCollection`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ task: taskCollection }) - .join( - leftJoin({ project: projectCollection }, ({ task, project }) => - eq(task.project_id, project.id), - ), + .leftJoin({ project: projectCollection }, ({ task, project }) => + eq(task.project_id, project.id), ), ) @@ -5057,10 +5036,8 @@ describe(`createLiveQueryCollection`, () => { const liveQuery = createLiveQueryCollection((q) => q .from({ task: taskCollection }) - .join( - leftJoin({ project: projectCollection }, ({ task, project }) => - eq(task.project_id, project.id), - ), + .leftJoin({ project: projectCollection }, ({ task, project }) => + eq(task.project_id, project.id), ), ) @@ -5144,10 +5121,8 @@ describe(`createLiveQueryCollection`, () => { query: (q) => q .from({ team: teamsCollection }) - .join( - leftJoin({ user: usersCollection }, ({ team, user }) => - eq(team.lead_id, user.id), - ), + .join({ user: usersCollection }, ({ team, user }) => + eq(team.lead_id, user.id), ) .select(({ team, user }) => ({ teamName: team.name, diff --git a/packages/db/tests/query/load-subset-join-dedupe.test.ts b/packages/db/tests/query/load-subset-join-dedupe.test.ts index c0e40daf1f..46a3b75747 100644 --- a/packages/db/tests/query/load-subset-join-dedupe.test.ts +++ b/packages/db/tests/query/load-subset-join-dedupe.test.ts @@ -1,9 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { - createLiveQueryCollection, - eq, - leftJoin, -} from '../../src/query/index.js' +import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { BasicIndex } from '../../src/indexes/basic-index.js' import { extractSimpleComparisons } from '../../src/query/expression-helpers.js' @@ -96,10 +92,8 @@ function createJoinedQuery( const live = createLiveQueryCollection((query) => query .from({ parent: parentCollection }) - .join( - leftJoin({ child: childCollection }, ({ parent, child }) => - eq(child.parentId, parent.id), - ), + .join({ child: childCollection }, ({ parent, child }) => + eq(child.parentId, parent.id), ), ) cleanups.push(() => live.cleanup()) diff --git a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts index 08d07d7ef1..7f116f78ba 100644 --- a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts +++ b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts @@ -3,9 +3,7 @@ import { describe, expect, it, vi } from 'vitest' import { createLiveQueryCollection, eq, - innerJoin, isUndefined, - leftJoin, not, toArray, } from '../../src/query/index.js' @@ -927,12 +925,10 @@ describe(`loadSubset replay refinement`, () => { const live = createLiveQueryCollection((q) => q .from({ primary: primary.collection }) - .join( - innerJoin( - { secondary: secondary.collection }, - ({ primary: left, secondary: right }) => - eq(left.joinKey, right.joinKey), - ), + .innerJoin( + { secondary: secondary.collection }, + ({ primary: left, secondary: right }) => + eq(left.joinKey, right.joinKey), ) .orderBy(({ primary: row }) => row.version) .limit(1) @@ -1592,17 +1588,13 @@ async function checkOrderedReplay( const from = q.from({ row: source }) const filtered = scenario.joined === `inner` - ? from.join( - innerJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId), - ), + ? from.innerJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId), ) : scenario.joined ? from - .join( - leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId), - ), + .leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId), ) .where(({ marker }) => scenario.joined === `none` diff --git a/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts b/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts index 008e556c16..ceedcd364a 100644 --- a/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts +++ b/packages/db/tests/query/load-subset-source-readiness-refinement-oracle.test.ts @@ -1,5 +1,5 @@ import { expect, it } from 'vitest' -import { BTreeIndex, innerJoin } from '../../src/index.js' +import { BTreeIndex } from '../../src/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { extractSimpleComparisons } from '../../src/query/expression-helpers.js' @@ -406,10 +406,8 @@ it.each([`resolve`, `reject`, `cleanup`] as const)( query: (q) => q .from({ left }) - .join( - innerJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.group, rightRow.group), - ), + .innerJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.group, rightRow.group), ) .select(({ left: leftRow, right: rightRow }) => ({ leftId: leftRow.id, diff --git a/packages/db/tests/query/load-subset-subquery.test.ts b/packages/db/tests/query/load-subset-subquery.test.ts index 8976748430..a9e8c1f94a 100644 --- a/packages/db/tests/query/load-subset-subquery.test.ts +++ b/packages/db/tests/query/load-subset-subquery.test.ts @@ -4,7 +4,6 @@ import { coalesce, createLiveQueryCollection, eq, - fullJoin, gt, gte, inArray, @@ -288,12 +287,8 @@ describe(`loadSubset with subqueries`, () => { // Use subquery in main query return q .from({ charge: chargesCollection }) - .join( - fullJoin( - { prepaidOrder: prepaidOrderQ }, - ({ charge, prepaidOrder }) => - eq(charge.address_id, prepaidOrder.address_id), - ), + .fullJoin({ prepaidOrder: prepaidOrderQ }, ({ charge, prepaidOrder }) => + eq(charge.address_id, prepaidOrder.address_id), ) }) cleanups.push(ordersCollection, subqueryQuery) @@ -366,12 +361,8 @@ describe(`loadSubset with subqueries`, () => { // Use subquery in main query return q .from({ charge: chargesCollection }) - .join( - fullJoin( - { prepaidOrder: prepaidOrderQ }, - ({ charge, prepaidOrder }) => - eq(charge.address_id, prepaidOrder.address_id), - ), + .fullJoin({ prepaidOrder: prepaidOrderQ }, ({ charge, prepaidOrder }) => + eq(charge.address_id, prepaidOrder.address_id), ) }) cleanups.push(ordersCollection, subqueryQuery) @@ -416,10 +407,8 @@ describe(`loadSubset with subqueries`, () => { return q .from({ charge: chargesCollection }) - .join( - fullJoin({ order: orderedOrders }, ({ charge, order }) => - eq(charge.address_id, order.address_id), - ), + .fullJoin({ order: orderedOrders }, ({ charge, order }) => + eq(charge.address_id, order.address_id), ) }) cleanups.push(ordersCollection, query) diff --git a/packages/db/tests/query/optimizer-semantics-oracle.test.ts b/packages/db/tests/query/optimizer-semantics-oracle.test.ts index 33500588d0..1b66ea04fd 100644 --- a/packages/db/tests/query/optimizer-semantics-oracle.test.ts +++ b/packages/db/tests/query/optimizer-semantics-oracle.test.ts @@ -22,13 +22,7 @@ * The nested matching cases also reject an all-empty QueryRef implementation. */ import { describe, expect, test } from 'vitest' -import { joinRuntime } from '../../src/query/compiler/join-runtime.js' -import { - createLiveQueryCollection, - innerJoin, - leftJoin, - rightJoin, -} from '../../src/query/index.js' +import { createLiveQueryCollection } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { optimizeQuery } from '../../src/query/optimizer.js' import { @@ -115,7 +109,7 @@ describe('optimizer aggregate semantics', () => { })) const joined = q .from({ s: summary }) - .join(innerJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target))) + .innerJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)) if (scenario.filter === `accept`) { return joined .where(({ s }) => eq(s.k, 2)) @@ -146,9 +140,7 @@ describe('optimizer aggregate semantics', () => { query: (q) => q .from({ s: aggregate }) - .join( - innerJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)), - ) + .innerJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)) .select(({ s }) => ({ total: s.total })), }) expect(materialized.toArray.map(stripVirtualProps)).toEqual( @@ -208,7 +200,7 @@ describe('optimizer aggregate semantics', () => { query: (q) => q .from({ s: aggregate }) - .join(leftJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target))) + .leftJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)) .where(({ s }) => eq(s.k, 2)) .select(({ s }) => ({ total: s.total })), }) @@ -231,9 +223,7 @@ describe('optimizer aggregate semantics', () => { })) return q .from({ s: summary }) - .join( - leftJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)), - ) + .leftJoin({ a: anchor }, ({ s, a }) => eq(s.total, a.target)) .where(({ s }) => outerPredicate === `accept` ? eq(s.k, 2) : eq(s.k, 1), ) @@ -283,7 +273,7 @@ describe('optimizer aggregate semantics', () => { : base.select(({ b }) => ({ k: 1, tag: `x`, total: sum(b.v) })) return q .from({ s: summary }) - .join(leftJoin({ a: anchor }, ({ s, a }) => eq(s.k, a.target))) + .leftJoin({ a: anchor }, ({ s, a }) => eq(s.k, a.target)) .where(({ s }) => eq(s.tag, `x`)) .select(({ s }) => ({ k: s.k, total: s.total })) }, @@ -325,7 +315,7 @@ describe('optimizer aggregate semantics', () => { .select(({ b }) => ({ k: b.k, total: add(sum(b.v), 0) })) return q .from({ s: summary }) - .join(leftJoin({ a: anchor }, ({ s, a }) => eq(s.k, a.target))) + .leftJoin({ a: anchor }, ({ s, a }) => eq(s.k, a.target)) .where(({ s }) => eq(s.k, 1)) .select(({ s }) => ({ k: s.k, total: s.total })) }, @@ -509,7 +499,6 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - runtime: joinRuntime, type: join.type, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -574,7 +563,6 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -583,7 +571,6 @@ describe(`optimizer residual convergence`, () => { ]), }, { - runtime: joinRuntime, type: `inner`, from: new CollectionRef(collection, `tag`), on: new Func('eq', [ @@ -631,7 +618,6 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - runtime: joinRuntime, type: joinType, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -664,7 +650,6 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -698,7 +683,6 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -728,7 +712,6 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -767,7 +750,6 @@ describe(`optimizer residual convergence`, () => { from: new CollectionRef(collection, `team`), join: [ { - runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -776,7 +758,6 @@ describe(`optimizer residual convergence`, () => { ]), }, { - runtime: joinRuntime, type: laterJoin, from: new CollectionRef(collection, `tag`), on: new Func('eq', [ @@ -838,7 +819,6 @@ describe(`optimizer residual convergence`, () => { ), join: [ { - runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -878,7 +858,6 @@ describe(`optimizer residual convergence`, () => { ), join: [ { - runtime: joinRuntime, type: `left`, from: new CollectionRef(collection, `member`), on: new Func('eq', [ @@ -937,10 +916,8 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .join( - leftJoin({ member: memberCollection }, ({ team, member }) => - eq(team.id, member.teamId), - ), + .leftJoin({ member: memberCollection }, ({ team, member }) => + eq(team.id, member.teamId), ) .where(({ team }) => eq(team.active, true)) .where(() => new Value(false)), @@ -999,10 +976,8 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .join( - leftJoin({ member: memberCollection }, ({ team, member }) => - eq(team.id, member.teamId), - ), + .leftJoin({ member: memberCollection }, ({ team, member }) => + eq(team.id, member.teamId), ) .where(({ team }) => eq(team.active, true)) .where(({ member }) => @@ -1018,11 +993,8 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .join( - rightJoin( - { member: memberCollection }, - ({ team, member }) => eq(team.id, member.teamId), - ), + .rightJoin({ member: memberCollection }, ({ team, member }) => + eq(team.id, member.teamId), ) .where(({ member }) => eq(member.userId, 100)) .where(({ team }) => @@ -1099,15 +1071,11 @@ describe(`optimizer residual convergence`, () => { query: (q) => q .from({ team: teamCollection }) - .join( - leftJoin({ member: memberCollection }, ({ team, member }) => - eq(team.id, member.teamId), - ), + .leftJoin({ member: memberCollection }, ({ team, member }) => + eq(team.id, member.teamId), ) - .join( - innerJoin({ tag: tagCollection }, ({ team, tag }) => - eq(team.id, tag.teamId), - ), + .innerJoin({ tag: tagCollection }, ({ team, tag }) => + eq(team.id, tag.teamId), ) .where(({ member }) => or(isUndefined(member.id), eq(member.userId, 100)), diff --git a/packages/db/tests/query/optimizer.test.ts b/packages/db/tests/query/optimizer.test.ts index fb89d407f4..80ce1e91f6 100644 --- a/packages/db/tests/query/optimizer.test.ts +++ b/packages/db/tests/query/optimizer.test.ts @@ -1,5 +1,4 @@ import { describe, expect, test } from 'vitest' -import { joinRuntime } from '../../src/query/compiler/join-runtime.js' import { grouping } from '../../src/query/compiler/grouping.js' import { optimizeQuery } from '../../src/query/optimizer.js' import { @@ -136,7 +135,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -168,7 +166,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -211,7 +208,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new QueryRef(distinctSource, `p`), type: `inner`, on: createEq(createPropRef(`u`, `id`), createPropRef(`p`, `id`)), @@ -233,7 +229,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -282,7 +277,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -322,7 +316,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -360,7 +353,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -398,7 +390,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -442,7 +433,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -503,7 +493,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -538,7 +527,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -580,7 +568,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -618,7 +605,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(existingSubquery, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -664,7 +650,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(deeplyNestedQuery, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -693,7 +678,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -719,7 +703,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -775,7 +758,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -799,7 +781,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -835,7 +816,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -879,7 +859,6 @@ describe(`Query Optimizer`, () => { ), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -928,7 +907,6 @@ describe(`Query Optimizer`, () => { ), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -985,7 +963,6 @@ describe(`Query Optimizer`, () => { ), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1023,7 +1000,6 @@ describe(`Query Optimizer`, () => { ), join: [ { - runtime: joinRuntime, from: new QueryRef( { from: new CollectionRef(mockCollection, `p`), @@ -1104,7 +1080,6 @@ describe(`Query Optimizer`, () => { ), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1138,7 +1113,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(mockCollection, `u`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1175,7 +1149,6 @@ describe(`Query Optimizer`, () => { // Add JOIN and WHERE to make it optimizable deepQuery.join = [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq(createPropRef(`u`, `id`), createPropRef(`p`, `user_id`)), @@ -1202,7 +1175,6 @@ describe(`Query Optimizer`, () => { ), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1259,7 +1231,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(sharedSubquery, `main_users`), join: [ { - runtime: joinRuntime, from: new QueryRef(sharedSubquery, `other_users`), // Same subquery object! type: `inner`, on: createEq( @@ -1320,7 +1291,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1364,7 +1334,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithLimitedOrder, `top_users`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1402,7 +1371,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithSelect, `filtered_users`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1451,7 +1419,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithHaving, `dept_stats`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1487,7 +1454,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithFnSelect, `computed_users`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1536,7 +1502,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithOrderOnly, `sorted_users`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1589,7 +1554,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(safeSubquery, `users`), join: [ { - runtime: joinRuntime, from: new QueryRef(unsafeSubquery, `dept_stats`), type: `inner`, on: createEq( @@ -1637,7 +1601,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1698,7 +1661,6 @@ describe(`Query Optimizer`, () => { from: new QueryRef(subqueryWithAggregates, `stats`), join: [ { - runtime: joinRuntime, from: new CollectionRef(mockCollection, `p`), type: `inner`, on: createEq( @@ -1789,7 +1751,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(teamsCollection, `team`), join: [ { - runtime: joinRuntime, type: `left`, from: new CollectionRef(teamMembersCollection, `teamMember`), on: createEq( @@ -1835,7 +1796,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(usersCollection, `user`), join: [ { - runtime: joinRuntime, type: `right`, from: new CollectionRef(profilesCollection, `profile`), on: createEq( @@ -1886,7 +1846,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(ordersCollection, `order`), join: [ { - runtime: joinRuntime, type: `full`, from: new CollectionRef(paymentsCollection, `payment`), on: createEq( @@ -1932,7 +1891,6 @@ describe(`Query Optimizer`, () => { from: new CollectionRef(usersCollection, `user`), join: [ { - runtime: joinRuntime, type: `inner`, from: new CollectionRef(departmentsCollection, `dept`), on: createEq( diff --git a/packages/db/tests/query/optional-fields-negative.test-d.ts b/packages/db/tests/query/optional-fields-negative.test-d.ts index 8fcb6037a4..b27e0743ad 100644 --- a/packages/db/tests/query/optional-fields-negative.test-d.ts +++ b/packages/db/tests/query/optional-fields-negative.test-d.ts @@ -1,11 +1,5 @@ import { describe, expectTypeOf, test } from 'vitest' -import { - createLiveQueryCollection, - eq, - gt, - innerJoin, - leftJoin, -} from '../../src/query/index.js' +import { createLiveQueryCollection, eq, gt } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' import type { OutputWithVirtual } from '../utils.js' @@ -91,10 +85,12 @@ describe(`Optional Fields - Type Safety Tests`, () => { const query = createLiveQueryCollection({ query: (q) => q.from({ user: usersCollection }).join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => { + { dept: departmentsCollection }, + ({ user, dept }) => { // This should work correctly - department_id is optional but can be used in join return eq(user.department_id, dept.id) - }), + }, + `inner`, ), }) @@ -167,9 +163,9 @@ describe(`Optional Fields - Type Safety Tests`, () => { q .from({ user: usersCollection }) .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `left`, ) .select(({ user, dept }) => ({ user_name: user.name, diff --git a/packages/db/tests/query/optional-fields-runtime.test.ts b/packages/db/tests/query/optional-fields-runtime.test.ts index 63f8376362..7bdcb7b838 100644 --- a/packages/db/tests/query/optional-fields-runtime.test.ts +++ b/packages/db/tests/query/optional-fields-runtime.test.ts @@ -3,7 +3,6 @@ import { createLiveQueryCollection, eq, groupKeys, - leftJoin, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -165,9 +164,9 @@ describe(`Optional Fields - Runtime Tests`, () => { q .from({ user: usersCollection }) .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `left`, ) .select(({ user, dept }) => ({ user_name: user.name, diff --git a/packages/db/tests/query/order-by.test.ts b/packages/db/tests/query/order-by.test.ts index ca3f2f88f2..9fba6e6e86 100644 --- a/packages/db/tests/query/order-by.test.ts +++ b/packages/db/tests/query/order-by.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it } from 'vitest' -import { leftJoin } from '@tanstack/db' import { groupKeys } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' @@ -834,11 +833,9 @@ function createOrderByTests(autoIndex: `off` | `eager`): void { q .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .orderBy(({ departments }) => departments.name, `asc`) .orderBy(({ employees }) => employees.salary, `desc`) @@ -1905,11 +1902,9 @@ function createOrderByTests(autoIndex: `off` | `eager`): void { q .from({ employees: employeesCollection }) .join( - leftJoin( - { departments: departmentsCollection }, - ({ employees, departments }) => - eq(employees.department_id, departments.id), - ), + { departments: departmentsCollection }, + ({ employees, departments }) => + eq(employees.department_id, departments.id), ) .orderBy(({ departments }) => departments.name, `asc`) .limit(5) @@ -1946,11 +1941,8 @@ function createOrderByTests(autoIndex: `off` | `eager`): void { const collection = createLiveQueryCollection((q) => q .from({ employee: employeesCollection }) - .join( - leftJoin( - { manager: employeesCollection }, - ({ employee, manager }) => eq(employee.id, manager.id), - ), + .join({ manager: employeesCollection }, ({ employee, manager }) => + eq(employee.id, manager.id), ) .orderBy(({ manager }) => manager.name, `asc`) .limit(3) diff --git a/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts b/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts index 1ed7ab1c27..1ffa8c8234 100644 --- a/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts +++ b/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts @@ -4,9 +4,7 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { createLiveQueryCollection, eq, - innerJoin, isUndefined, - leftJoin, not, toArray, } from '../../src/query/index.js' @@ -1117,10 +1115,8 @@ describe(`synchronous initial settlement refinement`, () => { query: (q) => q .from({ row: rows }) - .join( - leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId), - ), + .leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId), ) .where(({ marker }) => not(isUndefined(marker.rowId))) .fn.where(({ row }) => row.rank > 0) @@ -1223,10 +1219,8 @@ describe(`synchronous initial settlement refinement`, () => { query: (q) => q .from({ left }) - .join( - leftJoin({ right }, ({ left: leftRow, right: rightRow }) => - eq(leftRow.group, rightRow.group), - ), + .leftJoin({ right }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.group, rightRow.group), ) .orderBy(({ left: leftRow }) => leftRow.rank) .limit(2) @@ -2026,10 +2020,8 @@ async function observeLaterFallback( query: (q) => q .from({ root: rootCollection }) - .join( - leftJoin({ marker: markerCollection }, ({ root, marker }) => - eq(root.id, marker.rootId), - ), + .leftJoin({ marker: markerCollection }, ({ root, marker }) => + eq(root.id, marker.rootId), ) .where(({ marker }) => not(isUndefined(marker.id))) .orderBy(({ root }) => root.at, { @@ -3080,9 +3072,7 @@ function createFullSourceQuery( switch (feature) { case `inner-join`: return base - .join( - innerJoin({ tag: tags }, ({ row, tag }) => eq(row.tag, tag.tag)), - ) + .innerJoin({ tag: tags }, ({ row, tag }) => eq(row.tag, tag.tag)) .orderBy(({ row }) => row.rank) .limit(2) .select(({ row }) => ({ id: row.id })) diff --git a/packages/db/tests/query/ordered-work-oracle.property.test.ts b/packages/db/tests/query/ordered-work-oracle.property.test.ts index dc14adf5c9..1c73f7bb73 100644 --- a/packages/db/tests/query/ordered-work-oracle.property.test.ts +++ b/packages/db/tests/query/ordered-work-oracle.property.test.ts @@ -1,11 +1,6 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, it, vi } from 'vitest' -import { - fullJoin as fullJoinOperator, - innerJoin, - leftJoin, - toArray, -} from '../../src/query/index.js' +import { toArray } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { localOnlyCollectionOptions } from '../../src/local-only.js' import { createDeferred } from '../../src/deferred.js' @@ -472,10 +467,8 @@ async function observeConsumer( const query = (q: InitialQueryBuilder) => { const joined = q .from({ row: source }) - .join( - leftJoin({ marker: markerSource }, ({ row, marker }) => - eq(row.id, joinFilter === `to-one` ? marker.id : marker.rowId), - ), + .leftJoin({ marker: markerSource }, ({ row, marker }) => + eq(row.id, joinFilter === `to-one` ? marker.id : marker.rowId), ) .where(({ row, marker }) => { switch (joinFilter) { @@ -1950,10 +1943,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .join( - leftJoin({ marker }, ({ row, marker: child }) => - eq(row.id, child.rowId), - ), + .leftJoin({ marker }, ({ row, marker: child }) => + eq(row.id, child.rowId), ) .orderBy(({ row }) => row.rank) .limit(1) @@ -2048,10 +2039,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .join( - leftJoin({ marker: child }, ({ row, marker }) => - eq(row.id, marker.rowId), - ), + .leftJoin({ marker: child }, ({ row, marker }) => + eq(row.id, marker.rowId), ) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) @@ -2119,10 +2108,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .join( - leftJoin({ marker: joined }, ({ row, marker }) => - eq(row.id, marker.rowId), - ), + .leftJoin({ marker: joined }, ({ row, marker }) => + eq(row.id, marker.rowId), ) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) @@ -2246,10 +2233,8 @@ describe(`ordered source work oracle`, () => { .from({ order: orders }) .where(({ order }) => gte(order.scheduledAt, `2024-01-12`)) .where(({ order }) => eq(order.status, `queued`)) - .join( - innerJoin({ charge: charges }, ({ order, charge }) => - eq(order.addressId, charge.addressId), - ), + .innerJoin({ charge: charges }, ({ order, charge }) => + eq(order.addressId, charge.addressId), ), ) @@ -2371,10 +2356,8 @@ describe(`ordered source work oracle`, () => { const query = (q: InitialQueryBuilder) => q .from({ row: source }) - .join( - innerJoin({ marker: markers }, ({ row, marker }) => - eq(row.id, marker.rowId), - ), + .innerJoin({ marker: markers }, ({ row, marker }) => + eq(row.id, marker.rowId), ) .orderBy(({ row }) => row.rank) .limit(0) @@ -2621,12 +2604,10 @@ describe(`ordered source work oracle`, () => { const live = createLiveQueryCollection((q) => q .from({ primaryRow: primary }) - .join( - innerJoin( - { secondaryRow: secondary }, - ({ primaryRow, secondaryRow }) => - eq(primaryRow.joinKey, secondaryRow.joinKey), - ), + .innerJoin( + { secondaryRow: secondary }, + ({ primaryRow, secondaryRow }) => + eq(primaryRow.joinKey, secondaryRow.joinKey), ) .orderBy(({ primaryRow }) => primaryRow.rank) .limit(2), @@ -2744,12 +2725,10 @@ describe(`ordered source work oracle`, () => { q .from({ primaryRow: primary }) .where(({ primaryRow }) => eq(primaryRow.id, id)) - .join( - innerJoin( - { secondaryRow: secondary }, - ({ primaryRow, secondaryRow }) => - eq(primaryRow.joinKey, secondaryRow.joinKey), - ), + .innerJoin( + { secondaryRow: secondary }, + ({ primaryRow, secondaryRow }) => + eq(primaryRow.joinKey, secondaryRow.joinKey), ) .orderBy(({ primaryRow }) => primaryRow.rank) .limit(1), @@ -2902,10 +2881,8 @@ describe(`ordered source work oracle`, () => { const live = createLiveQueryCollection((q) => q .from({ row: primary }) - .join( - leftJoin({ child: secondary }, ({ row, child }) => - eq(row.id, child.primaryId), - ), + .leftJoin({ child: secondary }, ({ row, child }) => + eq(row.id, child.primaryId), ) .orderBy(({ row }) => row.rank) .limit(1) @@ -3157,10 +3134,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ root: rootCollection }) - .join( - leftJoin({ child: childCollection }, ({ root, child }) => - eq(root.id, child.rootId), - ), + .leftJoin({ child: childCollection }, ({ root, child }) => + eq(root.id, child.rootId), ) .where(({ child }) => not(isUndefined(child.rootId))) .orderBy(({ root }) => root.rank) @@ -3260,10 +3235,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ root: rootCollection }) - .join( - leftJoin({ child: childCollection }, ({ root, child }) => - eq(root.id, child.rootId), - ), + .leftJoin({ child: childCollection }, ({ root, child }) => + eq(root.id, child.rootId), ) .where(({ child }) => isUndefined(child.rootId)) .orderBy(({ root }) => root.rank) @@ -3436,10 +3409,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ root: rootCollection }) - .join( - leftJoin({ child: childCollection }, ({ root, child }) => - eq(root.id, child.rootId), - ), + .leftJoin({ child: childCollection }, ({ root, child }) => + eq(root.id, child.rootId), ) .where(({ child }) => isUndefined(child.rootId)) .orderBy(({ root }) => root.rank) @@ -4179,10 +4150,8 @@ describe(`ordered source work oracle`, () => { const query = (q: InitialQueryBuilder) => q .from({ row: root }) - .join( - leftJoin({ marker: markerSource }, ({ row, marker }) => - eq(row.id, marker.rowId), - ), + .leftJoin({ marker: markerSource }, ({ row, marker }) => + eq(row.id, marker.rowId), ) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) @@ -4294,10 +4263,8 @@ describe(`ordered source work oracle`, () => { query: (q) => q .from({ row: root }) - .join( - leftJoin({ marker: joined }, ({ row, marker }) => - eq(row.id, marker.rowId), - ), + .leftJoin({ marker: joined }, ({ row, marker }) => + eq(row.id, marker.rowId), ) .where(({ marker }) => isUndefined(marker.rowId)) .orderBy(({ row }) => row.rank) @@ -5532,20 +5499,16 @@ async function runEagerWindow(testCase: EagerCase): Promise<{ ) => { const joinedRows = q .from({ row: source }) - .join( - leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.link, marker.rowId), - ), + .leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.link, marker.rowId), ) const ordered = ( fullJoin ? // Same aliases as the LEFT join; only the join kind differs. (q .from({ row: source }) - .join( - fullJoinOperator({ marker: markers }, ({ row, marker }) => - eq(row.link, marker.rowId), - ), + .fullJoin({ marker: markers }, ({ row, marker }) => + eq(row.link, marker.rowId), ) as unknown as typeof joinedRows) : joined ? admitsWhenMatched @@ -6601,10 +6564,8 @@ async function checkOptimisticWindow( .where(({ row }) => eq(row.eligible, true)) const left = q .from({ row: source }) - .join( - leftJoin({ marker: markers }, ({ row, marker }) => - eq(row.link, marker.rowId), - ), + .leftJoin({ marker: markers }, ({ row, marker }) => + eq(row.link, marker.rowId), ) const filtered = shape === `source` diff --git a/packages/db/tests/query/pagination-oracle.property.test.ts b/packages/db/tests/query/pagination-oracle.property.test.ts index 0d5cc9de81..c4d58cf271 100644 --- a/packages/db/tests/query/pagination-oracle.property.test.ts +++ b/packages/db/tests/query/pagination-oracle.property.test.ts @@ -1,6 +1,6 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect, it } from 'vitest' -import { BTreeIndex, innerJoin, leftJoin } from '../../src/index.js' +import { BTreeIndex } from '../../src/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' import { createLiveQueryCollection } from '../../src/query/live-query-collection.js' @@ -3006,10 +3006,8 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ parent: parentSource }) - .join( - innerJoin({ child: childSource }, ({ parent, child }) => - eq(parent.groupId, child.groupId), - ), + .innerJoin({ child: childSource }, ({ parent, child }) => + eq(parent.groupId, child.groupId), ) .orderBy(({ parent }) => parent.rank, `asc`) .orderBy(({ parent }) => parent.id, `asc`) @@ -3057,10 +3055,8 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ parent: parentSource }) - .join( - innerJoin({ child: childSource }, ({ parent, child }) => - eq(parent.groupId, child.groupId), - ), + .innerJoin({ child: childSource }, ({ parent, child }) => + eq(parent.groupId, child.groupId), ) .orderBy(({ parent }) => parent.rank, `asc`) .orderBy(({ parent }) => parent.id, `asc`) @@ -3108,10 +3104,8 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ parent: parentSource }) - .join( - leftJoin({ child: childSource }, ({ parent, child }) => - eq(parent.childId, child.id), - ), + .leftJoin({ child: childSource }, ({ parent, child }) => + eq(parent.childId, child.id), ) .orderBy(({ parent }) => parent.sourceRank, `asc`) .orderBy(({ child }) => child.score, `asc`) @@ -6551,10 +6545,8 @@ describe(`pagination recomputation oracle`, () => { const live = createLiveQueryCollection((query) => query .from({ scope }) - .join( - innerJoin({ row: source }, ({ scope: outer, row }) => - eq(outer.groupId, row.groupId), - ), + .innerJoin({ row: source }, ({ scope: outer, row }) => + eq(outer.groupId, row.groupId), ) .where(({ row }) => eq(row.keep, true)) .orderBy(({ row }) => row.label, { diff --git a/packages/db/tests/query/query-api-type-algebra.test-d.ts b/packages/db/tests/query/query-api-type-algebra.test-d.ts index 9dcd7f54c0..1eb7fe9212 100644 --- a/packages/db/tests/query/query-api-type-algebra.test-d.ts +++ b/packages/db/tests/query/query-api-type-algebra.test-d.ts @@ -13,14 +13,7 @@ * join, and nullable-leaf suites remain the broader compatibility owners. */ import { describe, expectTypeOf, test } from 'vitest' -import { - Query, - createLiveQueryCollection, - eq, - fullJoin, - leftJoin, - rightJoin, -} from '../../src/query/index.js' +import { Query, createLiveQueryCollection, eq } from '../../src/query/index.js' import type { Collection } from '../../src/collection/index.js' import type { Context, @@ -72,10 +65,8 @@ describe(`query API type algebra`, () => { const query = createLiveQueryCollection((q) => q .from({ row: rows }) - .join( - leftJoin({ department: departments }, ({ row, department }) => - eq(row.departmentId, department.id), - ), + .leftJoin({ department: departments }, ({ row, department }) => + eq(row.departmentId, department.id), ) .select(({ department }) => ({ department, @@ -133,18 +124,14 @@ describe(`query API type algebra`, () => { ) { const joinedBranchA = new Query() .from({ rowA: rowsA }) - .join( - leftJoin({ otherA: othersA }, ({ rowA, otherA }) => - eq(rowA.id, otherA.id), - ), + .leftJoin({ otherA: othersA }, ({ rowA, otherA }) => + eq(rowA.id, otherA.id), ) .select(({ otherA }) => ({ other: otherA })) const joinedBranchB = new Query() .from({ rowB: rowsB }) - .join( - leftJoin({ otherB: othersB }, ({ rowB, otherB }) => - eq(rowB.id, otherB.id), - ), + .leftJoin({ otherB: othersB }, ({ rowB, otherB }) => + eq(rowB.id, otherB.id), ) .select(({ otherB }) => ({ other: otherB })) const joinedUnion = new Query() @@ -200,10 +187,8 @@ describe(`query API type algebra`, () => { const query = createLiveQueryCollection((q) => q .from({ row: rows }) - .join( - leftJoin({ department: departments }, ({ row, department }) => - eq(row.departmentId, department.id), - ), + .leftJoin({ department: departments }, ({ row, department }) => + eq(row.departmentId, department.id), ) .select(({ department }) => ({ ...department })), ) @@ -232,13 +217,11 @@ describe(`query API type algebra`, () => { const joined = new Query() .from({ row: rows }) - .join( - leftJoin({ item: a }, ({ row, item }) => { - // @ts-expect-error Only the generic constraint is available. - void item.notGuaranteed - return eq(row.id, item.id) - }), - ) + .leftJoin({ item: a }, ({ row, item }) => { + // @ts-expect-error Only the generic constraint is available. + void item.notGuaranteed + return eq(row.id, item.id) + }) .where(({ item }) => { // @ts-expect-error Nullable generic refs expose only guaranteed fields. void item.notGuaranteed @@ -290,15 +273,13 @@ describe(`query API type algebra`, () => { const joinedBranchUnion = new Query() .unionAll(aRows, bRows) - .join( - leftJoin({ row: rows }, ({ id: itemId, row }) => { - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - // @ts-expect-error The joined source declares no other field. - void row.notGuaranteed - return eq(itemId, row.id) - }), - ) + .leftJoin({ row: rows }, ({ id: itemId, row }) => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + // @ts-expect-error The joined source declares no other field. + void row.notGuaranteed + return eq(itemId, row.id) + }) .where((refs) => { void refs.id void refs.row.id @@ -312,9 +293,7 @@ describe(`query API type algebra`, () => { const rightJoinedBranchUnion = new Query() .unionAll(aRows, bRows) - .join( - rightJoin({ row: rows }, ({ id: itemId, row }) => eq(itemId, row.id)), - ) + .rightJoin({ row: rows }, ({ id: itemId, row }) => eq(itemId, row.id)) .select(({ id: itemId }) => ({ id: itemId })) return { @@ -543,7 +522,7 @@ describe(`query API type algebra`, () => { }, ) const rightJoined = union - .join(rightJoin({ row: rows }, ({ id, row }) => eq(id, row.id))) + .rightJoin({ row: rows }, ({ id, row }) => eq(id, row.id)) .select( ({ exactNull, @@ -560,7 +539,7 @@ describe(`query API type algebra`, () => { }), ) const fullJoined = union - .join(fullJoin({ row: rows }, ({ id, row }) => eq(id, row.id))) + .fullJoin({ row: rows }, ({ id, row }) => eq(id, row.id)) .select( ({ exactNull, diff --git a/packages/db/tests/query/query-api-type-algebra.test.ts b/packages/db/tests/query/query-api-type-algebra.test.ts index 49511b8279..0d6b2b9c50 100644 --- a/packages/db/tests/query/query-api-type-algebra.test.ts +++ b/packages/db/tests/query/query-api-type-algebra.test.ts @@ -1,9 +1,5 @@ import { expect, test } from 'vitest' -import { - createLiveQueryCollection, - eq, - leftJoin, -} from '../../src/query/index.js' +import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -28,10 +24,8 @@ test(`an unmatched whole-object projection publishes undefined`, () => { query: (q) => q .from({ row: rows }) - .join( - leftJoin({ department: departments }, ({ row, department }) => - eq(row.departmentId, department.id), - ), + .leftJoin({ department: departments }, ({ row, department }) => + eq(row.departmentId, department.id), ) .select(({ department }) => ({ department })), }) @@ -75,18 +69,14 @@ test(`branch unions publish unmatched whole-object projections as undefined`, as const query = createLiveQueryCollection((q) => { const branchA = q .from({ rowA: rowsA }) - .join( - leftJoin({ otherA: othersA }, ({ rowA, otherA }) => - eq(rowA.id, otherA.id), - ), + .leftJoin({ otherA: othersA }, ({ rowA, otherA }) => + eq(rowA.id, otherA.id), ) .select(({ otherA }) => ({ other: otherA })) const branchB = q .from({ rowB: rowsB }) - .join( - leftJoin({ otherB: othersB }, ({ rowB, otherB }) => - eq(rowB.id, otherB.id), - ), + .leftJoin({ otherB: othersB }, ({ rowB, otherB }) => + eq(rowB.id, otherB.id), ) .select(({ otherB }) => ({ other: otherB })) diff --git a/packages/db/tests/query/query-once.test.ts b/packages/db/tests/query/query-once.test.ts index e8a8270b9a..4d668db501 100644 --- a/packages/db/tests/query/query-once.test.ts +++ b/packages/db/tests/query/query-once.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { eq, innerJoin, queryOnce } from '../../src/query/index.js' +import { eq, queryOnce } from '../../src/query/index.js' import { CollectionImpl, createCollection } from '../../src/collection/index.js' import { Query } from '../../src/query/builder/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -234,9 +234,9 @@ describe(`queryOnce`, () => { q .from({ user: usersCollection }) .join( - innerJoin({ post: postsCollection }, ({ user, post }) => - eq(user.id, post.authorId), - ), + { post: postsCollection }, + ({ user, post }) => eq(user.id, post.authorId), + `inner`, ) .select(({ user, post }) => ({ userName: user.name, diff --git a/packages/db/tests/query/query-while-syncing.test.ts b/packages/db/tests/query/query-while-syncing.test.ts index bb41d6b311..e7096ce5e3 100644 --- a/packages/db/tests/query/query-while-syncing.test.ts +++ b/packages/db/tests/query/query-while-syncing.test.ts @@ -1,11 +1,5 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { - createLiveQueryCollection, - eq, - gt, - innerJoin, - leftJoin, -} from '../../src/query/index.js' +import { createLiveQueryCollection, eq, gt } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createTransaction } from '../../src/transactions.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' @@ -324,9 +318,9 @@ describe(`Query while syncing`, () => { q .from({ user: usersCollection }) .join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `inner`, ) .select(({ user, dept }) => ({ user_name: user.name, @@ -450,10 +444,8 @@ describe(`Query while syncing`, () => { query: (q) => q .from({ user: usersCollection }) - .join( - leftJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), ) .select(({ user, dept }) => ({ user_name: user.name, @@ -585,14 +577,14 @@ describe(`Query while syncing`, () => { q .from({ c1: collection1 }) .join( - innerJoin({ c2: collection2 }, ({ c1, c2 }) => - eq(c1.id, c2.id), - ), + { c2: collection2 }, + ({ c1, c2 }) => eq(c1.id, c2.id), + `inner`, ) .join( - innerJoin({ c3: collection3 }, ({ c1, c3 }) => - eq(c1.id, c3.id), - ), + { c3: collection3 }, + ({ c1, c3 }) => eq(c1.id, c3.id), + `inner`, ) .select(({ c1 }) => ({ id: c1.id, @@ -927,9 +919,9 @@ describe(`Query while syncing`, () => { q .from({ user: usersCollection }) .join( - innerJoin({ dept: departmentsCollection }, ({ user, dept }) => - eq(user.department_id, dept.id), - ), + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `inner`, ) .select(({ user, dept }) => ({ user_name: user.name, diff --git a/packages/db/tests/query/scheduler.test.ts b/packages/db/tests/query/scheduler.test.ts index 97c0079b00..cea6cfba98 100644 --- a/packages/db/tests/query/scheduler.test.ts +++ b/packages/db/tests/query/scheduler.test.ts @@ -2,11 +2,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { createLiveQueryCollection, eq, - fullJoin, include, - innerJoin, isNull, - leftJoin, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { createDeferred } from '../../src/deferred.js' @@ -111,11 +108,7 @@ function setupLiveQueryCollections(id: string) { query: (q) => q .from({ user: users }) - .join( - leftJoin({ task: tasks }, ({ user, task }) => - eq(user.id, task.userId), - ), - ) + .join({ task: tasks }, ({ user, task }) => eq(user.id, task.userId)) .select(({ user, task }) => ({ userId: user.id, taskId: task.id, @@ -1094,9 +1087,9 @@ describe(`live query scheduler`, () => { const query = new Query() .from({ left: leftQuery }) .join( - innerJoin({ right: rightQuery ?? rightSource }, ({ left, right }) => - eq(left.id, right.id), - ), + { right: rightQuery ?? rightSource }, + ({ left, right }) => eq(left.id, right.id), + `inner`, ) .select(({ left, right }) => ({ id: left.id, @@ -1217,9 +1210,9 @@ describe(`live query scheduler`, () => { q .from({ left: liveQueryA }) .join( - fullJoin({ right: liveQueryB }, ({ left, right }) => - eq(left.id, right.id), - ), + { right: liveQueryB }, + ({ left, right }) => eq(left.id, right.id), + `full`, ) .select(({ left, right }) => ({ left: left.value, @@ -1309,9 +1302,9 @@ describe(`live query scheduler`, () => { q .from({ left: liveQueryA }) .join( - fullJoin({ right: collectionB }, ({ left, right }) => - eq(left.id, right.id), - ), + { right: collectionB }, + ({ left, right }) => eq(left.id, right.id), + `full`, ) .select(({ left, right }) => ({ left: left.value, @@ -1397,9 +1390,9 @@ describe(`live query scheduler`, () => { q .from({ left: liveQueryA }) .join( - fullJoin({ right: collectionB }, ({ left, right }) => - eq(left.id, right.id), - ), + { right: collectionB }, + ({ left, right }) => eq(left.id, right.id), + `full`, ) .select(({ left, right }) => ({ left: left.value, @@ -1831,10 +1824,8 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ account: accounts }) - .join( - leftJoin({ user: users }, ({ user, account }) => - eq(user.id, account.user_id), - ), + .join({ user: users }, ({ user, account }) => + eq(user.id, account.user_id), ) .select(({ account, user }) => ({ account: account, @@ -1858,10 +1849,8 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ accountWithUser: accountsWithUsers }) - .join( - leftJoin({ team: activeTeams }, ({ accountWithUser, team }) => - eq(team.team.account_id, accountWithUser.account.id), - ), + .leftJoin({ team: activeTeams }, ({ accountWithUser, team }) => + eq(team.team.account_id, accountWithUser.account.id), ) .select(({ accountWithUser, team }) => ({ account: accountWithUser.account, @@ -1957,7 +1946,7 @@ describe(`live query scheduler`, () => { query: (q) => q .from({ a: queryA }) - .join(leftJoin({ b: queryB }, ({ a, b }) => eq(a.id, b.id))) + .leftJoin({ b: queryB }, ({ a, b }) => eq(a.id, b.id)) .select(({ a, b }) => ({ id: a.id, aValue: a.value, diff --git a/packages/db/tests/query/select-spread.test.ts b/packages/db/tests/query/select-spread.test.ts index 7854c6af29..6efdfa92fe 100644 --- a/packages/db/tests/query/select-spread.test.ts +++ b/packages/db/tests/query/select-spread.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest' -import { createLiveQueryCollection, innerJoin } from '../../src/query/index.js' +import { createLiveQueryCollection } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' import { add, eq, upper } from '../../src/query/builder/functions.js' @@ -258,7 +258,7 @@ describe(`select spreads (runtime)`, () => { const collection = createLiveQueryCollection((q) => q .from({ m: messagesCollection }) - .join(innerJoin({ u: users }, ({ m, u }) => eq(m.id, u.id))) + .join({ u: users }, ({ m, u }) => eq(m.id, u.id), `inner`) .select(({ m, u }) => ({ ...m, ...u, diff --git a/packages/db/tests/query/subquery-user-value-oracle.test.ts b/packages/db/tests/query/subquery-user-value-oracle.test.ts index d20ab77d9c..9b27a203fe 100644 --- a/packages/db/tests/query/subquery-user-value-oracle.test.ts +++ b/packages/db/tests/query/subquery-user-value-oracle.test.ts @@ -4,7 +4,6 @@ import { count, createLiveQueryCollection, eq, - innerJoin, materialize, } from '../../src/query/index.js' import { buildQuery } from '../../src/query/builder/index.js' @@ -83,10 +82,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .distinct() return q .from({ person: persons.collection }) - .join( - innerJoin({ group: distinctGroups }, ({ person, group }) => - eq(person.groupId, group.groupId), - ), + .innerJoin({ group: distinctGroups }, ({ person, group }) => + eq(person.groupId, group.groupId), ) .where(({ person }) => eq(person.enabled, true)) .select(({ person, group }) => ({ @@ -179,10 +176,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .distinct() return q .from({ person: persons.collection }) - .join( - innerJoin({ group: distinctGroups }, ({ person, group }) => - eq(person.groupId, group.groupId), - ), + .innerJoin({ group: distinctGroups }, ({ person, group }) => + eq(person.groupId, group.groupId), ) .where(({ person }) => eq(person.enabled, true)) .select(({ person, group }) => ({ @@ -413,10 +408,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .distinct() return q .from({ person: people.collection }) - .join( - innerJoin({ group: groups }, ({ person, group }) => - eq(person.groupId, group.groupId), - ), + .innerJoin({ group: groups }, ({ person, group }) => + eq(person.groupId, group.groupId), ) .where(({ group }) => eq(group.$key, 2)) .select(({ group }) => ({ groupId: group.groupId })) @@ -450,10 +443,8 @@ describe('subquery boundaries preserve operators and user rows', () => { })) return q .from({ person: people.collection }) - .join( - innerJoin({ result: stats }, ({ person, result }) => - eq(person.id, result.stats.total), - ), + .innerJoin({ result: stats }, ({ person, result }) => + eq(person.id, result.stats.total), ) .where(({ result }) => eq(result.$key, 'single_group')) .select(({ result }) => ({ total: result.stats.total })) @@ -490,10 +481,8 @@ describe('subquery boundaries preserve operators and user rows', () => { })) return q .from({ person: persons.collection }) - .join( - innerJoin({ result: stats }, ({ person, result }) => - eq(person.id, result.stats.total), - ), + .innerJoin({ result: stats }, ({ person, result }) => + eq(person.id, result.stats.total), ) .where(({ result }) => eq(result.$key, 'single_group')) .select(({ person, result }) => ({ @@ -566,10 +555,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .select(({ item }) => ({ total: count(item.id) })) return q .from({ person: persons.collection }) - .join( - innerJoin({ result: stats }, ({ person, result }) => - eq(person.id, result.total), - ), + .innerJoin({ result: stats }, ({ person, result }) => + eq(person.id, result.total), ) .where(({ result }) => eq(result.$key, 'single_group')) .select(({ person, result }) => ({ @@ -636,10 +623,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .findOne() return q .from({ anchor: anchors.collection }) - .join( - innerJoin({ first: firstQuery }, ({ anchor, first: row }) => - eq(anchor.id, row.id), - ), + .innerJoin({ first: firstQuery }, ({ anchor, first: row }) => + eq(anchor.id, row.id), ) .select(({ first: row }) => ({ id: row.id })) }, @@ -653,11 +638,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .findOne() return q .from({ first: firstQuery }) - .join( - innerJoin( - { anchor: anchors.collection }, - ({ first: row, anchor }) => eq(row.id, anchor.id), - ), + .innerJoin({ anchor: anchors.collection }, ({ first: row, anchor }) => + eq(row.id, anchor.id), ) .select(({ first: row }) => ({ id: row.id })) }, @@ -717,10 +699,8 @@ describe('subquery boundaries preserve operators and user rows', () => { query: (q) => { const firstJoined = q .from({ person: persons.collection }) - .join( - innerJoin({ tag: tagRows.collection }, ({ person, tag }) => - eq(person.groupId, tag.groupId), - ), + .innerJoin({ tag: tagRows.collection }, ({ person, tag }) => + eq(person.groupId, tag.groupId), ) .orderBy(({ person }) => person.id, 'asc') .select(({ person, tag }) => ({ @@ -730,10 +710,8 @@ describe('subquery boundaries preserve operators and user rows', () => { .findOne() return q .from({ anchor: anchors.collection }) - .join( - innerJoin({ first: firstJoined }, ({ anchor, first }) => - eq(anchor.id, first.personId), - ), + .innerJoin({ first: firstJoined }, ({ anchor, first }) => + eq(anchor.id, first.personId), ) .select(({ anchor, first }) => ({ anchorId: anchor.id, @@ -836,10 +814,8 @@ describe('subquery boundaries preserve operators and user rows', () => { q .from({ person: persons.collection }) .where(({ person }) => eq(person.scope, anchor.id)) - .join( - innerJoin({ tag: tagRows.collection }, ({ person, tag }) => - eq(person.groupId, tag.groupId), - ), + .innerJoin({ tag: tagRows.collection }, ({ person, tag }) => + eq(person.groupId, tag.groupId), ) .orderBy(({ person }) => person.id, 'asc') .select(({ person, tag }) => ({ diff --git a/packages/db/tests/query/subset-error-matrix.test.ts b/packages/db/tests/query/subset-error-matrix.test.ts index 307c050029..0f803f1660 100644 --- a/packages/db/tests/query/subset-error-matrix.test.ts +++ b/packages/db/tests/query/subset-error-matrix.test.ts @@ -1,10 +1,5 @@ import { describe, expect, it } from 'vitest' -import { - createEffect, - createLiveQueryCollection, - eq, - leftJoin, -} from '../../src/index.js' +import { createEffect, createLiveQueryCollection, eq } from '../../src/index.js' import { createCollection } from '../../src/collection/index.js' import { BTreeIndex } from '../../src/indexes/btree-index.js' import { getLoadSubsetDemandKey } from '../../src/query/ir-stable-identity.js' @@ -152,10 +147,8 @@ function startEffect( query: (q) => q .from({ item: primary }) - .join( - leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId), - ), + .leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId), ), ...callbacks, }) @@ -183,10 +176,8 @@ function startLive( return createLiveQueryCollection((q) => q .from({ item: primary }) - .join( - leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId), - ), + .leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId), ), ) } @@ -440,10 +431,8 @@ describe(`loadSubset failure matrix`, () => { query: (q) => q .from({ item: parent }) - .join( - leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId), - ), + .leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId), ), onBatch: () => {}, onSourceError: (error) => sourceErrors.push(error), @@ -454,10 +443,8 @@ describe(`loadSubset failure matrix`, () => { ? createLiveQueryCollection((q) => q .from({ item: parent }) - .join( - leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId), - ), + .leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId), ), ) : undefined @@ -522,10 +509,8 @@ describe(`loadSubset failure matrix`, () => { const live = createLiveQueryCollection((q) => q .from({ item: parent }) - .join( - leftJoin({ child }, ({ item, child: childRow }) => - eq(item.id, childRow.parentId), - ), + .leftJoin({ child }, ({ item, child: childRow }) => + eq(item.id, childRow.parentId), ), ) const originalQueueMicrotask = globalThis.queueMicrotask diff --git a/packages/db/tests/query/union-all.test-d.ts b/packages/db/tests/query/union-all.test-d.ts index b211129b6c..903b0d7480 100644 --- a/packages/db/tests/query/union-all.test-d.ts +++ b/packages/db/tests/query/union-all.test-d.ts @@ -4,11 +4,7 @@ import { count, createLiveQueryCollection, eq, - fullJoin, gt, - innerJoin, - leftJoin, - rightJoin, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -337,7 +333,9 @@ describe(`unionAll types`, () => { return q .unionAll(messageRows, toolCallRows) .join( - innerJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)), + { user: users }, + ({ userId, user }) => eq(userId, user.id), + `inner`, ) .select(({ kind, id, user }) => ({ kind, @@ -373,9 +371,9 @@ describe(`unionAll types`, () => { const messagesWithUsers = q .from({ message: messages }) .join( - innerJoin({ user: users }, ({ message, user }) => - eq(message.userId, user.id), - ), + { user: users }, + ({ message, user }) => eq(message.userId, user.id), + `inner`, ) const toolCallRows = q.from({ toolCall: toolCalls }) @@ -404,9 +402,9 @@ describe(`unionAll types`, () => { const messagesWithUsers = q .from({ message: messages }) .join( - innerJoin({ user: users }, ({ message, user }) => - eq(message.userId, user.id), - ), + { user: users }, + ({ message, user }) => eq(message.userId, user.id), + `inner`, ) .select(({ message, user }) => ({ id: message.id, @@ -451,10 +449,8 @@ describe(`unionAll types`, () => { message: messages, toolCall: toolCalls, }) - .join( - leftJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ), + .leftJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), ), ) @@ -487,10 +483,8 @@ describe(`unionAll types`, () => { message: messages, toolCall: toolCalls, }) - .join( - rightJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ), + .rightJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), ), ) @@ -516,10 +510,8 @@ describe(`unionAll types`, () => { message: messages, toolCall: toolCalls, }) - .join( - fullJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ), + .fullJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), ), ) @@ -569,9 +561,7 @@ describe(`unionAll types`, () => { return q .unionAll(messageRows, toolCallRows) - .join( - rightJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)), - ) + .rightJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)) .select(({ kind, id, user }) => ({ kind, id, @@ -613,9 +603,7 @@ describe(`unionAll types`, () => { return q .unionAll(messageRows, toolCallRows) - .join( - fullJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)), - ) + .fullJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)) .select(({ kind, id, user }) => ({ kind, id, diff --git a/packages/db/tests/query/union-all.test.ts b/packages/db/tests/query/union-all.test.ts index 2f2d37c557..fd104e6004 100644 --- a/packages/db/tests/query/union-all.test.ts +++ b/packages/db/tests/query/union-all.test.ts @@ -6,12 +6,8 @@ import { count, createLiveQueryCollection, eq, - fullJoin, gt, include, - innerJoin, - leftJoin, - rightJoin, toArray, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' @@ -377,7 +373,9 @@ describe(`unionAll`, () => { return q .unionAll(messageRows, toolCallRows) .join( - innerJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)), + { user: users }, + ({ userId, user }) => eq(userId, user.id), + `inner`, ) .select(({ kind, id, user }) => ({ kind, @@ -432,7 +430,9 @@ describe(`unionAll`, () => { return q .unionAll(messageRows, toolCallRows) .join( - innerJoin({ user: users }, ({ userId, user }) => eq(userId, user.id)), + { user: users }, + ({ userId, user }) => eq(userId, user.id), + `inner`, ) .select(({ kind, id, user }) => ({ kind, @@ -920,9 +920,9 @@ describe(`unionAll`, () => { const visibleMessagesWithUsers = q .from({ message: messages }) .join( - innerJoin({ user: users }, ({ message, user }) => - eq(message.userId, user.id), - ), + { user: users }, + ({ message, user }) => eq(message.userId, user.id), + `inner`, ) .where(({ message }) => eq(message.kind, `visible`)) .select(({ message, user }) => ({ @@ -976,9 +976,10 @@ describe(`unionAll`, () => { toolCall: toolCalls, }) .join( - innerJoin({ user: users }, ({ message, toolCall, user }) => + { user: users }, + ({ message, toolCall, user }) => eq(coalesce(message.userId, toolCall.userId), user.id), - ), + `inner`, ) .select(({ message, toolCall, user }) => ({ messageId: message.id, @@ -1011,10 +1012,8 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .join( - leftJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ), + .leftJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), ) .select(({ message, toolCall, user }) => ({ eventId: coalesce(message.id, toolCall.id), @@ -1049,10 +1048,8 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .join( - rightJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ), + .rightJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), ) .orderBy(({ user }) => user.id), ) @@ -1091,10 +1088,8 @@ describe(`unionAll`, () => { message: messages, toolCall: toolCalls, }) - .join( - fullJoin({ user: users }, ({ message, toolCall, user }) => - eq(coalesce(message.userId, toolCall.userId), user.id), - ), + .fullJoin({ user: users }, ({ message, toolCall, user }) => + eq(coalesce(message.userId, toolCall.userId), user.id), ) .orderBy(({ message, toolCall, user }) => coalesce(message.timestamp, toolCall.timestamp, user.id), @@ -1140,9 +1135,10 @@ describe(`unionAll`, () => { toolCall: toolCalls, }) .join( - innerJoin({ user: users }, ({ message, toolCall, user }) => + { user: users }, + ({ message, toolCall, user }) => eq(coalesce(message.userId, toolCall.userId), user.id), - ), + `inner`, ) .select(({ message, toolCall, user }) => ({ id: coalesce(message.id, toolCall.id), @@ -1184,10 +1180,8 @@ describe(`unionAll`, () => { return q .from({ user: users }) - .join( - leftJoin({ event: events }, ({ user, event }) => - eq(user.id, event.userId), - ), + .leftJoin({ event: events }, ({ user, event }) => + eq(user.id, event.userId), ) .select(({ user, event }) => ({ userId: user.id, @@ -1239,10 +1233,8 @@ describe(`unionAll`, () => { return q .from({ user: users }) - .join( - leftJoin({ event: events }, ({ user, event }) => - eq(user.name, event.label), - ), + .leftJoin({ event: events }, ({ user, event }) => + eq(user.name, event.label), ) .select(({ user, event }) => ({ userId: user.id, @@ -1300,10 +1292,8 @@ describe(`unionAll`, () => { return q .from({ user: users }) - .join( - leftJoin({ event: events }, ({ user, event }) => - eq(user.id, event.userId), - ), + .leftJoin({ event: events }, ({ user, event }) => + eq(user.id, event.userId), ) .select(({ user, event }) => ({ userId: user.id, @@ -1409,11 +1399,8 @@ describe(`unionAll`, () => { createLiveQueryCollection((q) => q .from({ message: messages }) - .join( - leftJoin( - { toolCall: toolCalls, user: users }, - ({ message, toolCall }) => eq(message.id, toolCall.id), - ), + .join({ toolCall: toolCalls, user: users }, ({ message, toolCall }) => + eq(message.id, toolCall.id), ), ), ).toThrow(OnlyOneSourceAllowedError) diff --git a/packages/db/tests/query/validate-aliases.test.ts b/packages/db/tests/query/validate-aliases.test.ts index 2467f015c4..c25036d9df 100644 --- a/packages/db/tests/query/validate-aliases.test.ts +++ b/packages/db/tests/query/validate-aliases.test.ts @@ -2,8 +2,6 @@ import { beforeEach, describe, expect, test } from 'vitest' import { createLiveQueryCollection, eq, - innerJoin, - leftJoin, toArray, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' @@ -66,10 +64,8 @@ describe(`Alias validation in subqueries`, () => { query: (q) => { const locksAgg = q .from({ lock: locksCollection }) - .join( - leftJoin({ vote: votesCollection }, ({ lock, vote }) => - eq(lock._id, vote.lockId), - ), + .join({ vote: votesCollection }, ({ lock, vote }) => + eq(lock._id, vote.lockId), ) .select(({ lock }) => ({ _id: lock._id, @@ -78,10 +74,8 @@ describe(`Alias validation in subqueries`, () => { return q .from({ vote: votesCollection }) - .join( - leftJoin({ lock: locksAgg }, ({ vote, lock }) => - eq(lock._id, vote.lockId), - ), + .join({ lock: locksAgg }, ({ vote, lock }) => + eq(lock._id, vote.lockId), ) .select(({ vote, lock }) => ({ voteId: vote._id, @@ -107,10 +101,8 @@ describe(`Alias validation in subqueries`, () => { .select(({ item }) => ({ id: item._id })) return q .from({ item: locksCollection }) - .join( - innerJoin({ child: reusable }, ({ item, child }) => - eq(item._id, child.id), - ), + .innerJoin({ child: reusable }, ({ item, child }) => + eq(item._id, child.id), ) .select(({ item }) => ({ id: item._id })) }, @@ -129,15 +121,11 @@ describe(`Alias validation in subqueries`, () => { query: (q) => q .from({ lock: locksCollection }) - .join( - leftJoin({ vote: votesCollection }, ({ lock, vote }) => - eq(vote.lockId, lock._id), - ), + .join({ vote: votesCollection }, ({ lock, vote }) => + eq(vote.lockId, lock._id), ) - .join( - leftJoin({ vote: votesCollection }, ({ lock, vote }) => - eq(vote.lockId, lock._id), - ), + .join({ vote: votesCollection }, ({ lock, vote }) => + eq(vote.lockId, lock._id), ) .select(({ lock }) => ({ _id: lock._id })), }) @@ -176,10 +164,8 @@ describe(`Alias validation in subqueries`, () => { .from({ tool: votesCollection }) .select(({ tool }) => ({ rid: tool._id })), ) - .join( - innerJoin({ anchor: locksCollection }, ({ rid, anchor }) => - eq(rid, anchor._id), - ), + .innerJoin({ anchor: locksCollection }, ({ rid, anchor }) => + eq(rid, anchor._id), ) .select(({ rid, anchor }) => ({ rid, @@ -226,10 +212,8 @@ describe(`Alias validation in subqueries`, () => { .from({ lock: locksCollection }) .select(({ lock }) => ({ id: lock._id })), ) - .join( - innerJoin({ vote: votesCollection }, ({ id, vote }) => - eq(vote._id, id), - ), + .innerJoin({ vote: votesCollection }, ({ id, vote }) => + eq(vote._id, id), ) .select(({ id, vote }) => ({ id, lockId: vote.lockId })), }) @@ -278,10 +262,8 @@ describe(`Alias validation in subqueries`, () => { const locksAgg = q .from({ lock: locksCollection }) .join( - leftJoin( - { v: votesCollection }, // Uses unique alias "v" instead of "vote" - ({ lock, v }) => eq(lock._id, v.lockId), - ), + { v: votesCollection }, // Uses unique alias "v" instead of "vote" + ({ lock, v }) => eq(lock._id, v.lockId), ) .select(({ lock }) => ({ _id: lock._id, @@ -290,10 +272,8 @@ describe(`Alias validation in subqueries`, () => { return q .from({ vote: votesCollection }) - .join( - leftJoin({ lock: locksAgg }, ({ vote, lock }) => - eq(lock._id, vote.lockId), - ), + .join({ lock: locksAgg }, ({ vote, lock }) => + eq(lock._id, vote.lockId), ) .select(({ vote, lock }) => ({ voteId: vote._id, diff --git a/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts b/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts index c1c715145f..c204c99901 100644 --- a/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts +++ b/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts @@ -37,7 +37,6 @@ import { describe, expectTypeOf, test } from 'vitest' import { createLiveQueryCollection, eq, - leftJoin, materialize, toArray, } from '../../src/query/index.js' @@ -228,10 +227,8 @@ describe(`virtual row field type boundary`, () => { unmatchedRows: toArray( q .from({ child: rows }) - .join( - leftJoin({ missing: rows }, ({ child, missing }) => - eq(child.id, missing.id), - ), + .leftJoin({ missing: rows }, ({ child, missing }) => + eq(child.id, missing.id), ) .select(({ missing }) => missing), ), diff --git a/packages/db/tests/query/virtual-row-fields-oracle.test.ts b/packages/db/tests/query/virtual-row-fields-oracle.test.ts index c2813fb78f..796f93f36c 100644 --- a/packages/db/tests/query/virtual-row-fields-oracle.test.ts +++ b/packages/db/tests/query/virtual-row-fields-oracle.test.ts @@ -36,7 +36,6 @@ import { describe, expect, test } from 'vitest' import { createLiveQueryCollection, eq, - leftJoin, materialize, toArray, } from '../../src/query/index.js' @@ -283,10 +282,8 @@ describe(`virtual row field runtime boundary`, () => { q .from({ child: rows }) .where(({ child }) => eq(child.id, row.id)) - .join( - leftJoin({ missing: missingRows }, ({ child, missing }) => - eq(child.id, missing.id), - ), + .leftJoin({ missing: missingRows }, ({ child, missing }) => + eq(child.id, missing.id), ) .select(({ missing }) => missing), ), diff --git a/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts b/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts index 470cb66895..f5cd7e90e5 100644 --- a/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts +++ b/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts @@ -5,7 +5,6 @@ import { createLiveQueryCollection, eq, getLoadSubsetDemandKey, - leftJoin, } from '@tanstack/db' import { QueryClient } from '@tanstack/query-core' import { describe, expect, it, vi } from 'vitest' @@ -1302,11 +1301,8 @@ async function expectEveryFailedSourceToRecover(): Promise { const live = createLiveQueryCollection((query) => query .from({ left: left.collection }) - .join( - leftJoin( - { right: right.collection }, - ({ left: leftRow, right: rightRow }) => eq(leftRow.id, rightRow.id), - ), + .join({ right: right.collection }, ({ left: leftRow, right: rightRow }) => + eq(leftRow.id, rightRow.id), ) .select(({ left: row }) => ({ id: row.id })), ) diff --git a/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts b/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts index b427bca2b5..3ba2478c4b 100644 --- a/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts +++ b/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts @@ -6,7 +6,6 @@ import { eq, getLoadSubsetDemandKey, isUndefined, - leftJoin, not, } from '@tanstack/db' import { QueryClient } from '@tanstack/query-core' @@ -640,10 +639,8 @@ async function startRun(testCase: Case): Promise { const window = createLiveQueryCollection((q) => { const ordered = q .from({ item: items }) - .join( - leftJoin({ marker: markerCollection }, ({ item, marker }) => - eq(item.id, marker.itemId), - ), + .leftJoin({ marker: markerCollection }, ({ item, marker }) => + eq(item.id, marker.itemId), ) .where(({ marker }) => not(isUndefined(marker.itemId))) .orderBy( diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index 8f56a96316..25292506e8 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -373,7 +373,7 @@ export function warnUnhashableDerivedIdentity( * const { data } = useLiveQuery({ * query: (q) => * q.from({ issues: issueCollection }) - * .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + * .join({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) * )) * .select(({ issues, persons }) => ({ diff --git a/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx b/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx index b834300049..c214bd3f01 100644 --- a/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx +++ b/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx @@ -89,10 +89,7 @@ import { createCollection, eq, getStableQueryBuilderHash, - innerJoin, isUndefined, - joinOf, - leftJoin, prepareLiveQueryValue, toArray, } from '@tanstack/db' @@ -190,11 +187,7 @@ describe(`standalone descriptor query binding`, () => { firstRenderDeferrals.clear() const continued = prepared - .join( - leftJoin({ late: lateDescriptor }, ({ base, late }) => - eq(base.id, late.id), - ), - ) + .join({ late: lateDescriptor }, ({ base, late }) => eq(base.id, late.id)) .select(({ base, late }) => ({ id: base.id, value: late.value })) expect(lateStarts).toBe(0) @@ -242,10 +235,8 @@ describe(`standalone descriptor query binding`, () => { client, deferrals, ) as typeof original - prepared.join( - leftJoin({ late: lateDescriptor }, ({ base, late }) => - eq(base.id, late.id), - ), + prepared.join({ late: lateDescriptor }, ({ base, late }) => + eq(base.id, late.id), ) throw never } @@ -866,10 +857,8 @@ describe(`standalone descriptor query binding`, () => { const joined = () => new Query() .from({ parent: parentDescriptor }) - .join( - innerJoin({ child: childDescriptor }, ({ parent, child }) => - eq(parent.id, child.parentId), - ), + .innerJoin({ child: childDescriptor }, ({ parent, child }) => + eq(parent.id, child.parentId), ) .select(({ parent, child }) => ({ id: parent.id, @@ -1355,15 +1344,13 @@ describe(`standalone descriptor query binding`, () => { const child = new Query() .from({ [alias]: mains }) .join( - joinOf( - kind, - { anchor: joinedSource }, - (context: Record) => - eq( - (context[alias] as Main).anchorId, - (context.anchor as Anchor).id, - ), - ), + { anchor: joinedSource }, + (context: Record) => + eq( + (context[alias] as Main).anchorId, + (context.anchor as Anchor).id, + ), + kind, ) .where((context: Record) => correlation === `joined` diff --git a/packages/react-db/tests/source-id-reuse-oracle.test.tsx b/packages/react-db/tests/source-id-reuse-oracle.test.tsx index 26edcd2533..a480d960c8 100644 --- a/packages/react-db/tests/source-id-reuse-oracle.test.tsx +++ b/packages/react-db/tests/source-id-reuse-oracle.test.tsx @@ -34,7 +34,6 @@ import { createCollection, eq, gt, - leftJoin, } from '@tanstack/db' import { act, render, renderHook, waitFor } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' @@ -151,21 +150,14 @@ describe(`React source ID reuse`, () => { include ? q .from({ failingA: failing[0]! }) - .join( - leftJoin( - { failingB: failing[1]! }, - ({ failingA, failingB }) => eq(failingA.id, failingB.id), - ), + .join({ failingB: failing[1]! }, ({ failingA, failingB }) => + eq(failingA.id, failingB.id), ) - .join( - leftJoin({ healthy }, ({ failingA, healthy }) => - eq(failingA.id, healthy.id), - ), + .join({ healthy }, ({ failingA, healthy }) => + eq(failingA.id, healthy.id), ) - .join( - leftJoin({ settings: current }, ({ failingA, settings }) => - eq(failingA.id, settings.id), - ), + .join({ settings: current }, ({ failingA, settings }) => + eq(failingA.id, settings.id), ) : q.from({ settings: current }), }) @@ -234,10 +226,8 @@ describe(`React source ID reuse`, () => { includeShared ? q .from({ settings: current }) - .join( - leftJoin({ shared: descriptor }, ({ settings, shared }) => - eq(settings.id, shared.id), - ), + .join({ shared: descriptor }, ({ settings, shared }) => + eq(settings.id, shared.id), ) : q.from({ settings: current }), }) @@ -349,10 +339,8 @@ describe(`React source ID reuse`, () => { query: (q) => q .from({ first: leftSource }) - .join( - leftJoin({ second: rightSource }, ({ first, second }) => - eq(first.id, second.id), - ), + .join({ second: rightSource }, ({ first, second }) => + eq(first.id, second.id), ), }), ), @@ -738,10 +726,8 @@ describe(`React source ID reuse: useLiveInfiniteQuery`, () => { includeShared ? q .from({ settings: current }) - .join( - leftJoin({ shared: descriptor }, ({ settings, shared }) => - eq(settings.id, shared.id), - ), + .join({ shared: descriptor }, ({ settings, shared }) => + eq(settings.id, shared.id), ) .orderBy(({ settings }) => settings.rank) : q diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index c7f9fd1aeb..98bf04309e 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -10,7 +10,6 @@ import { eq, gt, include, - leftJoin, localOnlyCollectionOptions, lte, sum, @@ -449,10 +448,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -826,10 +823,8 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -1466,10 +1461,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -1894,10 +1887,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -2650,10 +2641,8 @@ describe(`Query Collections`, () => { return q .from({ persons: personCollection }) - .join( - leftJoin({ ic: issueCountSubquery }, ({ persons, ic }) => - eq(persons.id, ic.userId), - ), + .leftJoin({ ic: issueCountSubquery }, ({ persons, ic }) => + eq(persons.id, ic.userId), ) .select(({ persons, ic }) => ({ name: persons.name, diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 15e4080a15..ec53cdb791 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -63,7 +63,7 @@ type InferConditionalResultType = * // Join pattern * const personIssues = useLiveQuery((q) => * q.from({ issues: issueCollection }) - * .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + * .join({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) * )) * .select(({ issues, persons }) => ({ diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index f7aac8db1e..743b91afcf 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -7,7 +7,6 @@ import { eq, gt, include, - leftJoin, toArray, waitForPersistedInitialRender, } from '@tanstack/db' @@ -384,10 +383,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -870,10 +867,8 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -1446,10 +1441,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -1846,10 +1839,8 @@ describe(`Query Collections`, () => { return useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index 6df2cc4a32..a5c89be708 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -177,7 +177,7 @@ function toValue(value: MaybeGetter): T { * // Join pattern * const issuesQuery = useLiveQuery((q) => * q.from({ issues: issueCollection }) - * .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + * .join({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) * )) * .select(({ issues, persons }) => ({ diff --git a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts index 91d6dc03aa..01c0e07e5e 100644 --- a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts +++ b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts @@ -8,7 +8,6 @@ import { getLiveQueryHash, getStableValueHash, gt, - leftJoin, } from '@tanstack/db' import { afterEach, describe, expect, it } from 'vitest' import { flushSync } from 'svelte' @@ -490,10 +489,8 @@ describe(`Query Collections`, () => { const query = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -778,10 +775,8 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -1295,10 +1290,8 @@ describe(`Query Collections`, () => { const query = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -1849,10 +1842,8 @@ describe(`Query Collections`, () => { const query = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 77fffa5d8c..72119dae51 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -150,7 +150,7 @@ export interface UseLiveQueryReturnWithSingleResultCollection< * // Join pattern * const { data } = useLiveQuery((q) => * q.from({ issues: issueCollection }) - * .join(leftJoin({ persons: personCollection }, ({ issues, persons }) => + * .join({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) * )) * .select(({ issues, persons }) => ({ diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index 472490969a..1379506972 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -5,7 +5,6 @@ import { createOptimisticAction, eq, gt, - leftJoin, } from '@tanstack/db' import { describe, expect, it } from 'vitest' import { nextTick, ref, watchEffect } from 'vue' @@ -317,10 +316,8 @@ describe(`Query Collections`, () => { const { state } = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -608,10 +605,8 @@ describe(`Query Collections`, () => { const queryResult = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -1182,10 +1177,8 @@ describe(`Query Collections`, () => { const { isReady } = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, @@ -1711,10 +1704,8 @@ describe(`Query Collections`, () => { const { isLoading, state, data } = useLiveQuery((q) => q .from({ issues: issueCollection }) - .join( - leftJoin({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id), - ), + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id), ) .select(({ issues, persons }) => ({ id: issues.id, diff --git a/scripts/bench/incremental-update.ts b/scripts/bench/incremental-update.ts index 5599909503..c7284020ee 100644 --- a/scripts/bench/incremental-update.ts +++ b/scripts/bench/incremental-update.ts @@ -18,19 +18,12 @@ import type { SyncConfig, } from '../../packages/db/src/index.js' -// CI runs this script against the base commit too. Before join operators, -// `.join()` took the source and condition directly. // Before include(), a bare subquery in select was a Collection include. const includeWith = (builder: any): any => typeof (db as { include?: unknown }).include === `function` ? (db as any).include(builder) : builder -const joinWith = (builder: any, source: object, on: (refs: any) => any): any => - typeof (db as { leftJoin?: unknown }).leftJoin === `function` - ? builder.join((db as any).leftJoin(source, on)) - : builder.join(source, on) - type Issue = { id: number status: `open` | `closed` @@ -214,16 +207,15 @@ const queryCases: Array = [ scenario: `visible author update`, createQuery: ({ issues, users }) => createLiveQueryCollection((q) => - joinWith( - q - .from({ issue: issues }) - .where(({ issue }) => eq(issue.status, `open`)), - { author: users }, - ({ issue, author }: any) => eq(issue.authorId, author.id), - ) - .orderBy(({ issue }: any) => issue.createdAt, `desc`) + q + .from({ issue: issues }) + .where(({ issue }) => eq(issue.status, `open`)) + .join({ author: users }, ({ issue, author }) => + eq(issue.authorId, author.id), + ) + .orderBy(({ issue }) => issue.createdAt, `desc`) .limit(50) - .select(({ issue, author }: any) => ({ + .select(({ issue, author }) => ({ id: issue.id, title: issue.title, createdAt: issue.createdAt, diff --git a/scripts/check-doc-snippets.mjs b/scripts/check-doc-snippets.mjs index c3dfb6f885..8bcc4c927c 100644 --- a/scripts/check-doc-snippets.mjs +++ b/scripts/check-doc-snippets.mjs @@ -87,21 +87,23 @@ for (const pkg of readdirSync(join(root, `packages`))) { } } -const fence = /```(ts|tsx|typescript)([^\n]*)\n([\s\S]*?)```/g +// A fence closes with as many backticks as it opened with, so a ````ts +// example can hold a ``` block. +const fence = /^(`{3,})(ts|tsx|typescript)([^\n]*)\n([\s\S]*?)^\1[ \t]*$/gm const snippets = [] for (const file of markdown) { const text = readFileSync(file, `utf8`) for (const match of text.matchAll(fence)) { - if (match[2].includes(`nocheck`)) continue + if (match[3].includes(`nocheck`)) continue const line = text.slice(0, match.index).split(`\n`).length snippets.push({ file: relative(root, file), line, - code: match[3], + code: match[4], name: join( root, `.snippet-check`, - `${snippets.length}.${/<\/|<[A-Z][A-Za-z]*[ />]/.test(match[3]) ? `tsx` : `ts`}`, + `${snippets.length}.${/<\/|<[A-Z][A-Za-z]*[ />]/.test(match[4]) ? `tsx` : `ts`}`, ), }) } diff --git a/scripts/doc-snippets-baseline.json b/scripts/doc-snippets-baseline.json index 8e3e1f91a2..aaaef95e90 100644 --- a/scripts/doc-snippets-baseline.json +++ b/scripts/doc-snippets-baseline.json @@ -115,14 +115,11 @@ "7b7a71d57c11728b", "7f72a99dc93b8051", "8ff5fe45604eaa8f", - "98e2bf9459f8b8df", "9a984bcf2b1d9fdb", "9b2bc781e86d85b1", "a039dbf46e062521", "a383434319068fab", - "aaab165b58980f8b", "ade01970173359a8", - "b4709cef2a5aeb18", "bee5ef646a9eedf9", "bef58a069d2b56e4", "c03e9d1cb3359f13", @@ -205,7 +202,7 @@ "4e6338d120ce5848", "84e9b444488d7a6b", "92c7e96918d4a6f3", - "df1ef46a41d27548" + "d5a8a15e8ea25ff2" ], "docs/quick-start.md": [ "1c2fe1bfdc225f1f", diff --git a/scripts/size-scenarios/app-join.js b/scripts/size-scenarios/app-join.js index d27ce8949f..89a7ccc676 100644 --- a/scripts/size-scenarios/app-join.js +++ b/scripts/size-scenarios/app-join.js @@ -1,5 +1,5 @@ // The typical app, plus a live query that joins a second collection. -import { createCollection, eq, leftJoin } from '@tanstack/db' +import { createCollection, eq } from '@tanstack/db' import { queryCollectionOptions } from '@tanstack/query-db-collection' import { useLiveQuery } from '@tanstack/react-db' import { queryClient, todos } from './app.js' @@ -19,9 +19,7 @@ export function AssignedTodos() { const { data } = useLiveQuery((q) => q .from({ todo: todos }) - .join( - leftJoin({ user: users }, ({ todo, user }) => eq(todo.userId, user.id)), - ) + .join({ user: users }, ({ todo, user }) => eq(todo.userId, user.id)) .select(({ todo, user }) => ({ title: todo.title, owner: user.name })), ) return data diff --git a/scripts/test-minified-db.mjs b/scripts/test-minified-db.mjs index d725b77a93..7263a43ad4 100644 --- a/scripts/test-minified-db.mjs +++ b/scripts/test-minified-db.mjs @@ -91,7 +91,7 @@ const errorGroups = { ThrottleCallDroppedError: `ThrottleCallDroppedError`, DebounceCallDroppedError: `DebounceCallDroppedError`, OnMutateMustBeSynchronousError: `OnMutateMustBeSynchronousError`, - QueryBuilderError: `QueryBuilderError OnlyOneSourceAllowedError SubQueryMustHaveFromClauseError InvalidSourceError InvalidSourceTypeError JoinConditionMustBeEqualityError QueryMustHaveFromClauseError InvalidWhereExpressionError JoinOperatorRequiredError IncludeOperatorRequiredError`, + QueryBuilderError: `QueryBuilderError OnlyOneSourceAllowedError SubQueryMustHaveFromClauseError InvalidSourceError InvalidSourceTypeError JoinConditionMustBeEqualityError QueryMustHaveFromClauseError InvalidWhereExpressionError IncludeOperatorRequiredError`, QueryCompilationError: `QueryCompilationError DistinctRequiresSelectError FnSelectWithGroupByError UnsupportedFnSelectResultError UnsupportedRootScalarSelectError HavingRequiresGroupByError LimitOffsetRequireOrderByError CollectionInputNotFoundError DuplicateAliasInSubqueryError UnsupportedFromTypeError UnknownExpressionTypeError EmptyReferencePathError UnknownFunctionError JoinCollectionNotFoundError MissingAliasInputsError SetWindowRequiresOrderByError SetWindowFindOneLimitError GroupingOperatorRequiredError`, UnsafeAliasPathError: `UnsafeAliasPathError`, JoinError: `JoinError UnsupportedJoinTypeError InvalidJoinConditionSameSourceError InvalidJoinConditionSourceMismatchError InvalidJoinConditionLeftSourceError InvalidJoinConditionRightSourceError InvalidJoinCondition UnsupportedJoinSourceTypeError`, From e642360f5beab2acb0b9469e03a45c32067537fd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:11:40 +0000 Subject: [PATCH 25/32] ci: apply automated fixes --- examples/react/projects/README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/react/projects/README.md b/examples/react/projects/README.md index 17db72bfc6..7b1d62065b 100644 --- a/examples/react/projects/README.md +++ b/examples/react/projects/README.md @@ -78,7 +78,7 @@ Now that you have two routes you can use a `Link` component to navigate between To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. ```tsx -import { Link } from "@tanstack/react-router" +import { Link } from '@tanstack/react-router' ``` Then anywhere in your JSX you can use it like so: @@ -98,10 +98,10 @@ In the File Based Routing setup the layout is located in `src/routes/__root.tsx` Here is an example layout that includes a header: ```tsx -import { Outlet, createRootRoute } from "@tanstack/react-router" -import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" +import { Outlet, createRootRoute } from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' -import { Link } from "@tanstack/react-router" +import { Link } from '@tanstack/react-router' export const Route = createRootRoute({ component: () => ( @@ -132,9 +132,9 @@ For example: ```tsx const peopleRoute = createRoute({ getParentRoute: () => rootRoute, - path: "/people", + path: '/people', loader: async () => { - const response = await fetch("https://swapi.dev/api/people") + const response = await fetch('https://swapi.dev/api/people') return response.json() as Promise<{ results: { name: string @@ -180,16 +180,16 @@ Built on a TypeScript implementation of differential dataflow, TanStack DB provi This example uses Query Collections for server-state synchronization with tRPC: ```tsx -import { createCollection } from "@tanstack/react-db" -import { queryCollectionOptions } from "@tanstack/query-db-collection" -import { QueryClient } from "@tanstack/query-core" +import { createCollection } from '@tanstack/react-db' +import { queryCollectionOptions } from '@tanstack/query-db-collection' +import { QueryClient } from '@tanstack/query-core' const queryClient = new QueryClient() export const todoCollection = createCollection( queryCollectionOptions({ - id: "todos", - queryKey: ["todos"], + id: 'todos', + queryKey: ['todos'], queryFn: async () => { const todos = await trpc.todos.getAll.query() return todos.map((todo) => ({ @@ -228,7 +228,7 @@ const AddTodo = () => { onClick={() => todoCollection.insert({ id: crypto.randomUUID(), - text: "🔥 Make app faster", + text: '🔥 Make app faster', completed: false, }) } @@ -242,7 +242,7 @@ const AddTodo = () => { Use live queries to read data reactively across collections: ```tsx -import { eq, useLiveQuery } from "@tanstack/react-db" +import { eq, useLiveQuery } from '@tanstack/react-db' const Todos = () => { // Read data using live queries with cross-collection joins From 9287e2185ae60e1a1efe6e7dbb7aeadd5cfbd1f2 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 26 Oct 2026 08:00:10 -0600 Subject: [PATCH 26/32] fix: address the xhigh review of opt-in grouping, includes, and pooling - Pooling no longer ships in every adapter app. The live-query observer found a pooled view's source with `instanceof PooledLiveQuery`, which kept the pooled view in every app that uses a framework hook. Pooled views now register their source in a small core WeakMap. The bundle test's app includes every value the adapters import from @tanstack/db, and a test keeps that list in step with the adapters. - The aggregate-select oracle checks the operator laws over its select grammar: bare group keys without an aggregate throw 246, a bare query builder as a field, in a nested object, or in a conditional branch throws 247, and a wrapped include that is not a top-level select value throws. It declares these states with createOracleReach. - Export `groupingCompiler` and `includesRuntime` for hand-built IR. - Svelte and Vue skills wrap their child query in include(). The doc example check reads the TypeScript script of Svelte and Vue examples. - The doc example key keeps separator commas, so `f(a, -b)` and `f(a - b)` differ. The self-test uses current API names. - Restore LiveQueryCollectionConfig's JSDoc, the join JSDoc examples, and the comments in join.test-d.ts that the join revert dropped. - The guide and skill no longer call a Collection include the default. The glossary says that pooling needs `pool`, and adds the operator terms. - A test checks at the adapter boundary that queries without `pool` keep a source subscription each. It fails on the merge-base, which pools by default. Co-authored-by: Isaac --- .changeset/opt-in-operators.md | 2 +- docs/contributing/glossary.md | 5 +- docs/contributing/oracle-coverage.md | 30 +- docs/guides/live-queries.md | 4 +- .../db/skills/db-core/live-queries/SKILL.md | 2 +- packages/db/src/live-query-observer.ts | 2 +- packages/db/src/query/builder/types.ts | 2 +- packages/db/src/query/index.ts | 11 + packages/db/src/query/live/types.ts | 26 +- packages/db/src/query/pooled-live-query.ts | 9 +- packages/db/src/query/pooled-source.ts | 24 + packages/db/tests/live-query-options.test.ts | 20 + .../db/tests/opt-in-operator-bundle.test.ts | 71 ++- .../aggregate-select-oracle.property.test.ts | 187 ++++++- .../query/hand-built-ir-operators.test.ts | 33 ++ packages/db/tests/query/join.test-d.ts | 44 +- packages/react-db/src/useLiveQuery.ts | 2 +- packages/solid-db/src/useLiveQuery.ts | 2 +- packages/svelte-db/skills/svelte-db/SKILL.md | 12 +- packages/svelte-db/src/useLiveQuery.svelte.ts | 2 +- packages/vue-db/skills/vue-db/SKILL.md | 12 +- packages/vue-db/src/useLiveQuery.ts | 2 +- scripts/check-doc-snippets.mjs | 18 +- scripts/doc-snippet-syntax.mjs | 69 ++- scripts/doc-snippets-baseline.json | 485 +++++++++--------- 25 files changed, 711 insertions(+), 365 deletions(-) create mode 100644 packages/db/src/query/pooled-source.ts create mode 100644 packages/db/tests/query/hand-built-ir-operators.test.ts diff --git a/.changeset/opt-in-operators.md b/.changeset/opt-in-operators.md index 1333913335..ba3f73881d 100644 --- a/.changeset/opt-in-operators.md +++ b/.changeset/opt-in-operators.md @@ -12,4 +12,4 @@ Breaking: grouping, includes, and pooling are now opt-in. An app bundles an oper - An include needs `include()`, `toArray()`, or `materialize()`. Change `issues: q.from(...)` in `select()` to `issues: include(q.from(...))`. A subquery in `select()` without one of these throws `IncludeOperatorRequiredError`. - Aggregates such as `count()` need no change. A `groupBy` with no aggregate takes `groupKeys()`: `.groupBy(groupKeys(({ todo }) => todo.status))`. Without it, the query throws `GroupingOperatorRequiredError`. - Hooks do not pool live queries by default. To share one partition among many equality-filtered queries, pass `pool: pooledLiveQueries` in the hook's query config: `useLiveQuery({ query, pool: pooledLiveQueries })`. Results do not change. Angular's `{ params, query }` form takes `pool` the same way. For adapter authors: the `pool` option of `resolveLiveQueryValue` is now `pooledLiveQueries` or `false`, not a boolean. -- Code that builds query IR by hand must supply the operator code. `IR.Aggregate` takes the grouping compiler as its third argument. `IR.IncludesSubquery` takes the include runtime as its fifth argument. Queries that the query builder builds need no change. +- Code that builds query IR by hand must supply the operator code. `IR.Aggregate` takes `groupingCompiler` as its third argument. `IR.IncludesSubquery` takes `includesRuntime` as its fifth argument, before `parentFilters`. Import both from `@tanstack/db`. Queries that the query builder builds need no change. diff --git a/docs/contributing/glossary.md b/docs/contributing/glossary.md index e45217b638..3125eeeae0 100644 --- a/docs/contributing/glossary.md +++ b/docs/contributing/glossary.md @@ -19,9 +19,12 @@ production queues, caches, or semantic helpers merely to share their names. | Collection | The public keyed data container. Capitalize it when referring to the TanStack DB type. | Relation, table, or query result. | | source Collection | A Collection read by a query or adapter. | Source relation when the value is a public Collection. | | live-query Collection | A Collection whose rows are produced by a live query. | Query, observer, or result set. | -| pooled live query | A live query on one source Collection whose `where` has at least one `eq(field, literal)` conjunct and otherwise reads only the row, optionally ordered by the row's own fields without a limit, served from an equality partition; its live-query Collection is built only when read. | Cached query or shared live-query Collection. | +| pooled live query | A live query on one source Collection whose `where` has at least one `eq(field, literal)` conjunct and otherwise reads only the row, optionally ordered by the row's own fields without a limit, served from an equality partition when its hook config or adapter passes `pooledLiveQueries` as `pool`; its live-query Collection is built only when read. A query of this shape without `pool` is an ordinary live query. | Cached query or shared live-query Collection. | | equality partition | Source rows grouped by the `eq`-normalized values of one set of fields, shared by pooled live queries on the same source Collection with the same equality fields and order terms, including resolved comparison options. | Index or bucket relation. | | partition group | The rows of an equality partition whose fields equal one tuple of literals. | Bucket or active bucket. | +| operator value | A query-builder value that carries the code for the operator it names: an aggregate or `groupKeys()` carries the grouping compiler, `include()`, `toArray()`, and `materialize()` carry the include runtime, and `pooledLiveQueries` carries pooling. An app bundles an operator's code only when it imports such a value. | Plugin or lazy import. | +| grouping compiler | The code that compiles `groupBy`, `having`, and aggregates, carried by aggregates and `groupKeys()` and exported as `groupingCompiler`. | Group-by runtime. | +| include runtime | The include materializer and the Collection-facade adapter, carried by `include()`, `toArray()`, and `materialize()` and exported as `includesRuntime`. | Includes plugin. | | relation | An internal weighted multiset maintained by D2. | Collection. | | row | One keyed public Collection value or one relation value. Qualify source row, relation row, or public row when more than one kind appears. | Event or transaction. | | change message | One insert, update, or delete delivered through the Collection sync boundary. | Transaction or publication. | diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 444730bb29..ef4913e752 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -631,7 +631,7 @@ generic visitor behavior outside the named collectors. | Correlated include path identity | `packages/db/tests/query/includes-context-transport-oracle.test.ts` | A flat parent field and nested parent field reach one-level and nested `toArray` results independently; dotted ancestor aliases remain distinct through a grandchild route. Initial results and parent updates have exact public-value checks. A conditional projection gives [`a.b`] and [`a`, `b`] different include results and checks parent-key changes plus later child inserts. Removing the compiler's unique route-key allocator loses a public child result; restoring dotted-string deduplication at either builder site loses a distinct parent value. These fixed witnesses do not establish arbitrary path segments, every recursive source form, or every materialization form. | | Alias scope identity | `packages/db/tests/query/includes-oracle.property.test.ts` (`includes alpha-renaming across lexical scopes`), `packages/db/tests/query/includes-scope-identity-oracle.ts`, `packages/db/tests/query/includes-alias-shadowing-oracle.test.ts`, `packages/db/tests/query/includes-outer-join-route-work-oracle.test.ts`, `packages/db/tests/query/validate-aliases.test.ts`, [issue #1975 review](oracle-reviews/issue-1975-scope-identity.md) | Four topologies place a source beside a same-named source: a `from()` subquery with an include, a joined `unionAll()` branch with an include, a collapsed top-level pure wrapper with a join subquery, and an include directly on a joined `unionAll()`, which may reuse a branch alias. The grammar crosses zero to two joins, LEFT or INNER, zero to two predicates, plain, ordered-and-limited, or DISTINCT bodies, include forms, and eager or on-demand finite providers, after preload and after source writes. Includes read their source directly, through a join, through a `unionAll()` with an anchor join, or through a `from()` subquery that reads the parent row. One in four scenarios draws any naming; same-scope and union-branch repeats are rejected while ancestor shadowing is admitted. Each checkpoint compares the canonical naming with plain recomputation, the generated naming with the canonical naming, and per-Collection `loadSubset` WHERE clauses. The exact-output companion independently recomputes parent-child rows for direct, QueryRef, union, and implicit-join forms across two equality operand orders, shadowed and renamed aliases, eager and on-demand children, initial publication and parent/child writes. The recursive forms keep a required outer lockId correlation and an independent captured lockName predicate; the union has a nonempty right branch and a later two-branch overlap. It checks enumerable user and virtual key names, user values, multiplicity, sibling helper reuse, a fresh parent/inner QueryRef declaration across a source update, direct, child union-branch, parent union-branch, parent QueryRef, and nested-include rejection of ancestor/descendant builder reuse, one nested grandparent capture, functional callback child input, grouping, captured whole-row and nested-path spreads across shadowed and renamed aliases, eager and on-demand children, and parent/child writes, and query identity. Focused follow-up cells check same-path group keys under shadowing and renaming through parent and child writes, alpha-renamed pure wrappers at public-row and optimized-identity cuts, and indexed lazy demand for direct or wrapped nested includes through anchor and user writes. Wrong-query controls that drop either inner predicate or substitute a parent-shaped callback value fail at the initial public-row comparison. A finite follow-up crosses direct includes and joined QueryRefs with shadowed or renamed aliases, eager or on-demand sources, and child IDs equal to or distinct from an outer filter literal; it checks public rows and on-demand child admission after preload. Another follow-up projects a captured parent rank through a child QueryRef and checks public rows after preload, a child-rank change, and a parent-rank change. The original compiler misrouted the outer filter in four cases, and the original optimizer lost the captured binding in two cases at their first public-row comparison. Every pinned witness and both campaigns fail on `18abceee4`; each of the four optimizer re-mint sites fails a witness when reverted. Arbitrary nested includes, include-inside-subquery forms beyond the checked LEFT joined QueryRef, other join-subquery topologies, `having`, outer joins beyond the bounded RIGHT/FULL matrix, publication events, unload, ordered windows, other lazy join targets, and temporal demand remain outside these owners; they need witnesses before an alias-scope closure claim covers them. Ancestor/descendant builder reuse is rejected before correlation extraction; one helper builder remains legal across a parent QueryRef, union branch, or joined QueryRef and a sibling include child. Six placement/source-mode cells check construction, exact rows after preload, and a source update; the joined placement includes an unmatched manager that would expose a missing helper join. The original source-tree guard rejects all six. A discarded enumeration of a captured proxy cannot change the selected query identity or rows before or after a child update; the retained-marker implementation fails that identity check. The [PR #2079 follow-up](oracle-reviews/issue-2072-alias-shadowing-followup.md) records its RED trace, the repaired spread and nested-QueryRef cells, and the remaining compiler and optimizer evidence gaps. The [ad5e0108 review](oracle-reviews/issue-2072-alias-shadowing-ad5e0108.md) adds a self-join with an inner alias matching the outer joined source, a joined child whose local source competes with inherited parent context in matched and unmatched LEFT joins, captured/local spreads, grandparent whole-row capture, and direct captured-alias selection. These finite cells compare public rows after preload and selected writes; an alias-only lazy-target mutant fails the eager self-join comparison. The reported on-demand failure did not reproduce with the controlled source. A further finite matrix crosses nested FROM or JOIN placement, field or whole-row capture of a shadowed outer joined alias, shadowed or renamed spelling, and eager or on-demand acquisition. It compares source-role rows after preload, child deletion and movement, joined-row update and movement, and parent update; on-demand cases compare nonempty request sets. Disabling projected binding lookup fails all eight cells at the initial public-row assertion; an alias-only lazy-target mutant survives this matrix, so it adds no broader lazy-target claim. A bounded outer-join extension crosses RIGHT and FULL joins, main- or joined-side correlation, direct and QueryRef joined sources, shadowed and renamed aliases, eager and on-demand sources, two parent routes, and seven initial or post-write cuts. Its independent source-role model checks exact public rows, including both absent sides; on-demand cells also require source acquisition. A join-operand extension crosses an ancestor term combined with a local main or joined term, or serving as the whole main key, with equality reversal, alias renaming, source mode, and five public-row cuts. The [outer-join follow-up](oracle-reviews/issue-2072-alias-shadowing-outer-joins.md) records RED and GREEN evidence. A foreign captured join operand must reject: a finite provenance matrix checks root and include placements, matching or different alias spelling, and eager or on-demand sources at construction or preload settlement; valid local and captured-ancestor operands remain controls. A two-LEFT-join history now checks that a later routed QueryRef or captured operand cannot restore an absent shadowed local source, across alias renaming, eager/on-demand mode, two parents, and local and second-joined arrival/removal. It also selects and updates a parent label while joins are matched, checking that the main route retains that ancestor value. A nested joined QueryRef control projects its join key from an inner tag source while the post alias shadows or renames an outer user alias; an independent plain-row model and installed on-demand post/tag rows are compared at preload, a user-key move, and a tag update. A wrong Collection/SourceId pair loads an irrelevant post at preload and fails the installed-row check. Other nested source shapes and real-provider request schedules remain outside this control. The route-work owner compares public rows and assembled parent-route work as irrelevant joined rows grow for direct main-correlated RIGHT/FULL joins in both source modes; an unconditional-route mutant grows from two to six pairs at preload. RIGHT joins correlated on main now join the outer-join row matrix, beside joined-side correlation and FULL controls. An include child rejects a bound join operand declared inside its parent QueryRef or union branch, across matching/renamed aliases and eager/on-demand sources; the original source-tree collector resolves preload instead of rejecting. A joined QueryRef rejects a captured source declared beside it in the enclosing query: the matrix crosses root/include placement, matching/renamed inner alias, and eager/on-demand sources; four true-ancestor controls assert rows, and the permissive sibling-scope mutant fails all eight invalid cells while those controls pass. The primary oracle now extends the hidden-source rejection through two QueryRef or union levels, with matching/renamed aliases and both source modes; a source-tree-binding mutant fails all eight added cells at the rejection cut. An unrelated bound reference is rejected in WHERE, GROUP BY, HAVING, ORDER BY, direct SELECT, and conditional SELECT across matching/renamed aliases; legal local controls and a grouped include with captured ancestor predicates, HAVING, ordering, shadowing, and parent/child writes prevent a blanket rejection. It also rejects foreign references moved into extracted include parent filters or into the removed correlation equality, each across alias spelling and source mode with valid parent controls. Disabling expression validation fails twelve non-join cells; omitting parent-filter or extracted-correlation validation fails four cells each. A separate plain relational model checks RIGHT-to-LEFT, RIGHT-to-RIGHT, FULL-to-FULL, and FULL-to-LEFT two-join chains under shadowing/renaming, both source modes, main/joined/tag correlation, and six source or ancestor writes; a later-FULL-as-LEFT mutant fails two initial public-row comparisons. The route-work owner now compares public rows and parent-route assembly counts at preload and four main-source writes while irrelevant joined rows grow. Other expression forms, arbitrary join-subquery and lazy-target shapes, deeper captures, other chained outer joins, real-provider schedules, and broader source-work bounds remain open under this owner. The alias-scope owner also checks outer grouping through a nested QueryRef and joined source with shadowed or renamed inner alias, eager or on-demand acquisition, and four source/tag writes at five public-row cuts. The tag insertion duplicates one joined source and the plain model counts both copies. A controlled rewrite probe reached an unbound pushed WHERE inside the QueryRef while the bound GROUP BY remained outside; an inner GROUP BY blocked that pushdown. A matcher mutant that ignored binding IDs failed two same-path group cells at the first public-row cut; a dropped-pushed-predicate mutant failed all four new cells by publishing an extra group. This refutes the reported WHERE-rewrite-to-GROUP-BY-matcher route in those shapes; another lost-provenance route needs a concrete fixture. Base/head work probes confirmed an extra pure-wrapper stage after #2079, but the same full-source outer-WHERE request and keyed lazy-join request on both revisions. No wrapper-stage work budget or safe binding remap is established. Bound root refs caused two route-metadata symbol checks per evaluation on the current head versus zero on base, while captured-ancestor rows remain protected by the primary oracle; a compile-time fast path needs a justified work budget and a preservation witness. The same function-form subquery placed twice compiled two results and invoked its callback four times for two source rows on both revisions. Cache comparison read more query properties on the current head, and a stateful callback produced placement-specific values. A callback identity or purity rule and work budget are needed before cache-equivalence changes. | | Join equality and cold acquisition | `packages/db/tests/query/cold-join-reconciliation-oracle.test.ts` | Independent recomputation for cold acquisition plus direct join/predicate equivalence across established equality domains. Binary/string and nullish classes, replacement histories, raw on-demand values, and both scan/auto-index paths are explicit; the compound lane also covers tuple equality, term order, all four join types, controlled cold sources, and keyed histories. Positive acquisition witnesses require a satisfiable tuple. | -| Aggregate selects | [aggregate select oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/aggregate-select-oracle.property.test.ts) | Generated select shapes of aggregate queries with and without `groupBy` by `c.k`, `upper(c.k)`, `inArray(c.k, ['a', 'b'])`, a Date-valued `coalesce` or a `NaN`-valued `add`, rebuilt per use (literals, parent fields, the group key, the raw field `c.k`, source fields, `count`/`sum`/`max`, `add`, `caseWhen` with expression or object branches, nested objects, spreads, a nested include) and a HAVING condition that may compare an aggregate, selected as a field or not, at the top level, directly or through a `unionAll`, and in an include whose source may shadow the parent alias, through parent updates and source inserts, updates (including key changes that move a row to another group and route) and deletes. A shape throws `NonAggregateExpressionNotInGroupByError` exactly when a non-key source field appears outside an aggregate in its select or HAVING, or the shape spreads a row or holds a nested include; otherwise every published group equals the model and keeps the select's field order. Open: several group keys. | +| Aggregate selects | [aggregate select oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/aggregate-select-oracle.property.test.ts) | Generated select shapes of aggregate queries with and without `groupBy` by `c.k`, `upper(c.k)`, `inArray(c.k, ['a', 'b'])`, a Date-valued `coalesce` or a `NaN`-valued `add`, rebuilt per use (literals, parent fields, the group key, the raw field `c.k`, source fields, `count`/`sum`/`max`, `add`, `caseWhen` with expression or object branches, nested objects, spreads, a nested include) and a HAVING condition that may compare an aggregate, selected as a field or not, at the top level, directly or through a `unionAll`, and in an include whose source may shadow the parent alias, through parent updates and source inserts, updates (including key changes that move a row to another group and route) and deletes. A shape throws `NonAggregateExpressionNotInGroupByError` exactly when a non-key source field appears outside an aggregate in its select or HAVING, or the shape spreads a row or holds a nested include; otherwise every published group equals the model and keeps the select's field order. The same grammar checks the opt-in operator laws: a grouped query whose select and HAVING hold no aggregate throws `GroupingOperatorRequiredError` when its keys are passed bare, and works through `groupKeys()`; a bare query builder throws `IncludeOperatorRequiredError` as a field, inside a nested object, or in a conditional branch; a wrapped include that is not a top-level select value throws. When several errors apply, any one of them passes. The property declares these six states with `createOracleReach`, and each campaign of 120 histories reaches each of them 19 to 38 times. A mutant that skips the grouping-operator check fails it with a `TypeError`, and one that treats a bare builder as an include fails it with the wrong error class. Open: several group keys. The operator laws are generated only for aggregate and grouped selects; for plain selects, `includes.test.ts` pins a bare builder as a field, nested, and in a conditional, because this model publishes one row per group and needs a separate row-per-source model for them. | | Query optimizer semantics | `packages/db/tests/query/optimizer-semantics-oracle.test.ts`, [aggregate review](oracle-reviews/2026-09-28-optimizer-aggregate-pushdown.md), [QueryRef repair](oracle-reviews/2026-09-30-queryref-publication-repair.md), [residual review](oracle-reviews/2026-10-01-pr-1977-residual-convergence.md) | Independent sum recomputation and a materialized-Collection formulation check the first public snapshot of a nested aggregate under a left join. Direct, arithmetic-wrapped, and conditional aggregates distinguish safe from unsafe pushdown; a grouped-key control covers the grouped boundary. Matching and nonmatching global aggregates also cross a nested QueryRef inner join with absent, accepting, and rejecting outer predicates. The matching initial cases fail on the pre-repair revision and pass after the computed-projection lookup repair. A separate fixed residual-convergence grammar checks exact predicate and outer WHERE clause counts at `optimizeQuery` return and after re-optimization: LEFT, RIGHT, INNER, and FULL joins; equality, greater-than, nullable-field `isUndefined`, same-source OR, cross-source equality, clause order, separate/AND form, two active predicates, two residual sources, and an existing residual marker. Plain-array LEFT/RIGHT and three-source join models check first public rows, including unmatched rows. Arbitrary expressions, more complex join chains, and incremental optimizer/publication histories remain outside this owner. | | QueryRef operators and user-value boundaries | `packages/db/tests/query/subquery-user-value-oracle.test.ts`, `packages/db/tests/transactions.test.ts`, [repair record](oracle-reviews/2026-09-30-queryref-publication-repair.md) | A finite array/Set model checks the compiled output bag for a joined DISTINCT subquery with an outer WHERE. Live-query drivers check selected containers, no-select, functional, and predicate-literal user objects with IR-like fields, top-level and aggregate-subquery proxy-shaped fields, arrays of selected references, outer virtual-field filters on joined DISTINCT and nested aggregate QueryRefs, and a renamed no-select source. The transaction suite documents the existing development-browser duplicate-load guard. The bounded cases run in `test:oracles` or the full DB suite. Joined DISTINCT, nested and flat aggregate source-update histories pass. Ordered joined `findOne()` checks initial, singleton, empty, restored, and changed-join-key cuts; an unordered `findOne()` QueryRef on either side of a join checks default-key candidate deletion and reinsertion. A materialized joined `findOne()` supplies a separate receiving formulation. Other `singleResult` forms, join forms, schedules, and cross-copy value handling outside the duplicate-load guard remain unproved. | | Opaque backend pagination | [window oracle](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.oracle.test.ts), [cache histories](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.cache-oracle.test.ts), [cache publication](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.publication-oracle.test.ts), [browser acquisition boundaries](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.boundary-oracle.test.ts), [QueryCollection integration](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination-oracle.integration.test.ts) | Full filter/sort/slice reference, opaque token transport, actual Query cache expiry/invalidation/GC, forced refresh during growth, protocol failure publication/recovery, bounded slice work, nested cancellation/replacement, reader abort, browser retry defaults, manual-write cache isolation, and production window publications. Stable backend sequences; not snapshot guarantees for changing endpoints. Peek-ahead remains enabled. | @@ -3792,23 +3792,23 @@ query's old and new spellings on one input. Open counterexample, not fixed by this change: the ordered-window random campaign in `packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts` fails -with `TANSTACK_DB_ORACLE_SEED=1593300187` (path `121:1:2:3:2:2`). The -history is `hold`, `truncateMarkers`, `settle` with a load failure, over an -ascending limit-one window on an on-demand marker source with includes. The -convergence check sees a terminal error. Main at `65d9ada50` fails with the -same counterexample, so the operator change did not introduce it. A full -suite run on the branch also found it with seed `-1005431615` (path `68:1:2`), -which shrinks to the same history. The campaign reaches this defect in -ordinary runs, so CI can fail on it intermittently until the defect is fixed. Owner: the -load-subset replay owner. Needed witness: a decision whether a truncate -during a held, then failed, load may end the window in a terminal error, and -the fix or model change that follows. +with `TANSTACK_DB_ORACLE_SEED=1593300187` (path `121:1:2:3:2:2`) and with +seed `-1005431615` (path `68:1:2`). Both shrink to one history: `hold`, +`truncateMarkers`, `settle` with a load failure, over an ascending limit-one +window on an on-demand marker source with includes. The convergence check +sees a terminal error. Main at `65d9ada50` and the merge-base `427a7c31e` +fail with the same counterexample, so the operator change did not introduce +it. The campaign reaches this defect in ordinary runs, so CI can fail on it +intermittently until the defect is fixed. Owner: the load-subset replay +owner. Needed witness: a decision whether a truncate during a held, then +failed, load may end the window in a terminal error, and the fix or model +change that follows. Open observation without a trace: during this change, one run of the random campaign in `packages/db/tests/query/includes-oracle.property.test.ts` failed -on the branch. The run did not record its seed or counterexample. Five later -dedicated runs and two full-suite runs on the branch passed, and the campaign -was not run on `main` at that time. Owner: the includes oracle owner. Needed witness: the failing seed and +on the branch. The run did not record its seed or counterexample. Thirteen +later dedicated runs and two full-suite runs on the branch passed, and the +campaign was not run on `main` at that time. Owner: the includes oracle owner. Needed witness: the failing seed and path from a later run. Until then, this entry cannot tell an operator regression from an older defect. A later failure of that campaign must record its seed here before anyone calls it pre-existing. diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index 544181644a..f5b11b71da 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -1317,7 +1317,7 @@ Each project gets its own top-5 issues, not 5 issues shared across all projects. ### toArray -By default, each child result is a live `Collection`. If you want a plain array instead, wrap the child query with `toArray()`: +`include()` makes each child result a live `Collection`. If you want a plain array instead, wrap the child query with `toArray()`: ```ts import { createLiveQueryCollection, eq, toArray } from '@tanstack/db' @@ -1339,7 +1339,7 @@ const projectsWithIssues = createLiveQueryCollection((q) => ) ``` -With `toArray()`, the project row is re-emitted whenever its issues change. Without it, the child `Collection` updates independently. +With `toArray()`, the project row is re-emitted whenever its issues change. With `include()`, the child `Collection` updates independently. ### materialize diff --git a/packages/db/skills/db-core/live-queries/SKILL.md b/packages/db/skills/db-core/live-queries/SKILL.md index 677f6f4c42..dcadb2d7a5 100644 --- a/packages/db/skills/db-core/live-queries/SKILL.md +++ b/packages/db/skills/db-core/live-queries/SKILL.md @@ -235,7 +235,7 @@ Embed a correlated subquery inside `select()` to produce hierarchical (nested) data. The subquery must contain a `where` with an `eq()` that correlates a parent field with a child field. -### Collection includes (default) +### Collection includes with include() Return a child `Collection` on each parent row: diff --git a/packages/db/src/live-query-observer.ts b/packages/db/src/live-query-observer.ts index 3e8e15a6c8..49dbf6bb95 100644 --- a/packages/db/src/live-query-observer.ts +++ b/packages/db/src/live-query-observer.ts @@ -5,7 +5,7 @@ import { isSingleResultCollection, } from './live-query-adapter.js' import { getBuilderFromConfig } from './query/live/collection-registry.js' -import { getPooledLiveQuerySource } from './query/pooled-live-query.js' +import { getPooledLiveQuerySource } from './query/pooled-source.js' import { getPersistedReadinessSource } from './persisted-readiness.js' import type { Collection } from './collection/index.js' import type { DbClient, DehydratedLiveQueryResult } from './client.js' diff --git a/packages/db/src/query/builder/types.ts b/packages/db/src/query/builder/types.ts index 259317cd03..b92af21d6f 100644 --- a/packages/db/src/query/builder/types.ts +++ b/packages/db/src/query/builder/types.ts @@ -1354,7 +1354,7 @@ export type HasJoinType< * // users is NOT optional here - we can access users.id directly * // orders is NOT optional here - we can access orders.userId directly * return eq(users.id, orders.userId) - * })) + * }) * .where(({ orders }) => { * // NOW orders is optional because it's after the LEFT JOIN * return orders?.status === 'pending' diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index 319c2f898f..392ed2316a 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -45,6 +45,17 @@ export { export { groupKeys, type GroupKeys } from './builder/group-keys.js' export type { OperatorCallback } from './builder/types.js' +// Operator code for query IR built by hand: `IR.Aggregate` takes the grouping +// compiler, and `IR.IncludesSubquery` the include runtime. +export { + grouping as groupingCompiler, + type Grouping, +} from './compiler/grouping.js' +export { + includesRuntime, + type IncludesRuntime, +} from './live/includes-runtime.js' + // Expression functions exports export { // Operators diff --git a/packages/db/src/query/live/types.ts b/packages/db/src/query/live/types.ts index f9dc5b6ece..8d16063e88 100644 --- a/packages/db/src/query/live/types.ts +++ b/packages/db/src/query/live/types.ts @@ -63,19 +63,6 @@ export type FullSyncState = Required> & * } * ``` */ -/** - * A live query config that a framework hook accepts. Pass `pooledLiveQueries` - * as `pool` to serve the query from a shared equality partition when its - * shape allows. A config with any option other than `query`, `gcTime`, and - * `pool` compiles its own Collection. - */ -export type LiveQueryHookConfig< - TContext extends Context, - TResult extends object = RootQueryResult, -> = LiveQueryCollectionConfig & { - pool?: PooledLiveQueries -} - export interface LiveQueryCollectionConfig< TContext extends Context, TResult extends object = RootQueryResult, @@ -134,3 +121,16 @@ export interface LiveQueryCollectionConfig< */ defaultStringCollation?: StringCollationConfig } + +/** + * A live query config that a framework hook accepts. Pass `pooledLiveQueries` + * as `pool` to serve the query from a shared equality partition when its + * shape allows. A config with any option other than `query`, `gcTime`, and + * `pool` compiles its own Collection. + */ +export type LiveQueryHookConfig< + TContext extends Context, + TResult extends object = RootQueryResult, +> = LiveQueryCollectionConfig & { + pool?: PooledLiveQueries +} diff --git a/packages/db/src/query/pooled-live-query.ts b/packages/db/src/query/pooled-live-query.ts index 0cfaa4b321..fa610bcd85 100644 --- a/packages/db/src/query/pooled-live-query.ts +++ b/packages/db/src/query/pooled-live-query.ts @@ -9,6 +9,7 @@ import { equalityConjunct, equalityKey, readPath } from './equality-conjunct.js' import { compileExpression, toBooleanPredicate } from './compiler/evaluators.js' import { buildCompareOptions } from './compiler/order-by.js' import { createLiveQueryCollection } from './live-query-collection.js' +import { registerPooledSource } from './pooled-source.js' import type { BasicExpression, OrderBy, QueryIR } from './ir.js' import type { BaseQueryBuilder } from './builder/index.js' import type { Collection, CollectionImpl } from '../collection/index.js' @@ -496,6 +497,7 @@ class PooledLiveQuery { private readonly passes: ((row: Row) => boolean) | undefined, ) { partition.retain(gcTime) + registerPooledSource(this, source) } get status(): CollectionStatus { @@ -627,13 +629,6 @@ class PooledLiveQuery { } } -/** The one source of an adapter's pooled view, without materializing it. */ -export function getPooledLiveQuerySource( - value: unknown, -): CollectionImpl | undefined { - return value instanceof PooledLiveQuery ? value.source : undefined -} - // The observer reads the view itself; users get the live-query Collection. const forwardToCollection: ProxyHandler = { get(view, property) { diff --git a/packages/db/src/query/pooled-source.ts b/packages/db/src/query/pooled-source.ts new file mode 100644 index 0000000000..3ac427db12 --- /dev/null +++ b/packages/db/src/query/pooled-source.ts @@ -0,0 +1,24 @@ +import type { CollectionImpl } from '../collection/index.js' + +// A pooled view's source Collection. A view registers here when it is +// created, so the live-query observer finds the source without bundling +// pooling into apps that never import `pooledLiveQueries`. +const pooledSources = new WeakMap< + object, + CollectionImpl +>() + +export function registerPooledSource( + view: object, + source: CollectionImpl, +): void { + pooledSources.set(view, source) +} + +export function getPooledLiveQuerySource( + value: unknown, +): CollectionImpl | undefined { + return typeof value === `object` && value !== null + ? pooledSources.get(value) + : undefined +} diff --git a/packages/db/tests/live-query-options.test.ts b/packages/db/tests/live-query-options.test.ts index 4dc2e54d8c..aae279398a 100644 --- a/packages/db/tests/live-query-options.test.ts +++ b/packages/db/tests/live-query-options.test.ts @@ -459,6 +459,26 @@ describe(`pooling is opt-in`, () => { ).toBeUndefined() }) + // The public observation: every framework hook resolves its query here, so + // two equality-filtered queries without `pool` keep a source subscription + // each. On main before this change they shared one. + it(`keeps one source subscription per query without pool`, async () => { + const rows = source() + const queries = [1, 2].map((group) => + resolveLiveQueryValue( + new BaseQueryBuilder() + .from({ row: rows }) + .where(({ row }) => eq(row.group, group)), + )!, + ) + const subscriptions = queries.map((query) => + query.subscribeChanges(() => {}), + ) + await Promise.all(queries.map((query) => query.preload())) + expect(rows.subscriberCount).toBe(2) + for (const subscription of subscriptions) subscription.unsubscribe() + }) + it(`does not pool when the hook passes pool: false`, () => { const rows = source() expect( diff --git a/packages/db/tests/opt-in-operator-bundle.test.ts b/packages/db/tests/opt-in-operator-bundle.test.ts index cf7d00509a..aba13829f6 100644 --- a/packages/db/tests/opt-in-operator-bundle.test.ts +++ b/packages/db/tests/opt-in-operator-bundle.test.ts @@ -1,4 +1,6 @@ // @vitest-environment node +import { readFileSync, readdirSync } from 'node:fs' +import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { build } from 'esbuild' import { describe, expect, it } from 'vitest' @@ -81,7 +83,7 @@ async function bundledModules(names: Array) { } // `concat` and `caseWhen` build select values but bring no operator code. -const typicalApp = [ +const coreValues = [ `createLiveQueryCollection`, `eq`, `and`, @@ -90,6 +92,69 @@ const typicalApp = [ `createCollection`, ] +// Every value that a framework adapter imports from `@tanstack/db`. An +// adapter app bundles these whatever operators it imports, so none of them may +// bring an operator's code. `adapterImports()` below keeps this list current. +const adapterValues = [ + `BaseQueryBuilder`, + `UnhashableQueryIRError`, + `assertLiveQueryWindowManyResult`, + `compareLiveQueryWindowDependencies`, + `createEffect`, + `createLiveQueryCollection`, + `createLiveQueryObserver`, + `createLiveQueryWindowController`, + `createPacedMutations`, + `deepEquals`, + `fetchNextLiveQueryWindowPage`, + `getLiveQueryHash`, + `getLiveQueryWindowCollectionWarning`, + `getLiveQueryWindowInputKind`, + `getPreparedLiveQueryIdentity`, + `getPreparedLiveQuerySources`, + `getPublicCollection`, + `getStableValueHash`, + `isCollection`, + `isSingleResultCollection`, + `liveQueryWindowMatches`, + `normalizeLiveQueryWindowPageSize`, + `prepareLiveQueryValue`, + `resolveLiveQueryValue`, + `resolveLiveQueryWindowInput`, + `resumeDeferredLiveQueryCollections`, + `shouldPreserveLiveQueryWindowPageCount`, +] + +const typicalApp = [...new Set([...coreValues, ...adapterValues])] + +/** The values that the framework adapters import from `@tanstack/db`. */ +function adapterImports() { + const packages = fileURLToPath(new URL(`../..`, import.meta.url)) + const names = new Set() + const visit = (dir: string): void => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name) + if (entry.isDirectory()) visit(path) + else if (/\.tsx?$/.test(entry.name)) + for (const [, list] of readFileSync(path, `utf8`).matchAll( + /import\s*\{([^}]*)\}\s*from\s*'@tanstack\/db'/g, + )) + for (const name of list!.split(`,`).map((n) => n.trim())) + if (name && !name.startsWith(`type `)) + names.add(name.split(/\s+as\s+/)[0]!) + } + } + for (const adapter of [ + `react-db`, + `vue-db`, + `solid-db`, + `svelte-db`, + `angular-db`, + ]) + visit(join(packages, adapter, `src`)) + return [...names].sort() +} + /** The law: an operator's modules are in the bundle exactly when it is imported. */ function expectOperators( bytes: ReadonlyMap, @@ -102,6 +167,10 @@ function expectOperators( } describe(`opt-in operator bundles`, () => { + it(`lists every value the framework adapters import`, () => { + expect(adapterImports()).toEqual([...adapterValues].sort()) + }) + it.each( Object.entries(values).flatMap(([family, names]) => names.map((name) => [name, family as Family] as const), diff --git a/packages/db/tests/query/aggregate-select-oracle.property.test.ts b/packages/db/tests/query/aggregate-select-oracle.property.test.ts index 5df20e5805..840c81e004 100644 --- a/packages/db/tests/query/aggregate-select-oracle.property.test.ts +++ b/packages/db/tests/query/aggregate-select-oracle.property.test.ts @@ -17,10 +17,13 @@ import { import { createCollection } from '../../src/collection/index.js' import { AggregateFunctionNotInSelectError, + GroupingOperatorRequiredError, + IncludeOperatorRequiredError, NonAggregateExpressionNotInGroupByError, } from '../../src/errors.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' import { oraclePropertyOptions, oracleRuns } from '../oracle-config.js' +import { createOracleReach } from '../oracle-reach.js' /** * # What does an aggregate query publish beside its aggregates? @@ -83,8 +86,26 @@ import { oraclePropertyOptions, oracleRuns } from '../oracle-config.js' * can compare with key values, so a missing key changes the branch taken. Group order is not part of the law, so rows are * compared in a fixed order. * + * A query also needs the operator code its select and grouping use. Group + * keys passed to `groupBy` bare carry no grouping compiler, so a grouped + * query whose select and HAVING hold no aggregate throws + * `GroupingOperatorRequiredError` unless it wraps its keys in `groupKeys()`; + * any aggregate brings the compiler. A query builder in a select value is a + * subquery only inside `include()`, `toArray()`, or `materialize()`: a bare + * one throws `IncludeOperatorRequiredError` when the select is built, as a + * field, inside a nested object, or in a conditional branch. Authority: the + * maintainer's decision in PR #2232 that these operators are opt-in and that + * a missing operator throws. When several errors apply, the query throws one + * of them; which one is not part of the law. + * + * Each history chooses whether a grouped query passes its keys bare, and + * where a nested include sits and whether it is wrapped. The property declares + * the states these laws need with `createOracleReach` and fails when a + * campaign does not reach each of them. + * * Limits: one parent field (`x`), one source field (`v`) and one group key - * (`k`); numbers only. + * (`k`); numbers only. A nested include in a unionAll query is rejected + * before compilation, so the driver checks only that it throws. */ const PROPERTY = `aggregate.select-shapes` @@ -110,8 +131,11 @@ type Shape = { */ having?: { part: Part; lit: number | string; selected?: string } spread?: `source` | `parent` - /** A nested include beside the aggregate. */ - include?: boolean + /** + * A nested include beside the aggregate: wrapped in `toArray()` or bare, as + * a field, inside a nested object, or in both branches of a conditional. + */ + include?: { wrap: `toArray` | `bare`; at: `field` | `nested` | `case` } } /** @@ -214,13 +238,68 @@ function modelThrows(shape: Shape, grouping: Grouping): boolean { (shape.having?.selected === undefined && shape.having !== undefined && readsSourceOutsideAggregate(shape.having.part, grouping)) || - shape.include === true || + shape.include?.wrap === `toArray` || Object.values(shape.fields).some((part) => readsSourceOutsideAggregate(part, grouping), ) ) } +type ModelError = + | `nonAggregate` + | `aggregateNotInSelect` + | `includeRequired` + | `groupingRequired` + | `nestedInclude` + +/** An aggregate in the select or HAVING brings the grouping compiler. */ +function shapeHasAggregate(shape: Shape): boolean { + return ( + Object.values(shape.fields).some(hasAggregate) || + (shape.having !== undefined && + shape.having.selected === undefined && + hasAggregate(shape.having.part)) + ) +} + +/** Every error the model says applies. The query must throw one of them. */ +function modelErrors( + shape: Shape, + grouping: Grouping, + bareKeys: boolean, +): Set { + const errors = new Set() + // A bare query builder is no include; the select rejects it when built. + if (shape.include?.wrap === `bare`) errors.add(`includeRequired`) + // An include is a top-level select value (db-core skill, "Includes + // rules"), so one inside a nested object or a conditional branch throws. + if (shape.include?.wrap === `toArray` && shape.include.at !== `field`) + errors.add(`nestedInclude`) + // Bare group keys carry no grouping compiler, and no aggregate brings one. + if (grouping !== `none` && bareKeys && !shapeHasAggregate(shape)) + errors.add(`groupingRequired`) + const error = modelError(shape, grouping) + if (error) errors.add(error) + return errors +} + +function errorName(error: unknown): ModelError | `other` { + if (error instanceof IncludeOperatorRequiredError) return `includeRequired` + if (error instanceof GroupingOperatorRequiredError) return `groupingRequired` + if (error instanceof NonAggregateExpressionNotInGroupByError) + return `nonAggregate` + if (error instanceof AggregateFunctionNotInSelectError) + return `aggregateNotInSelect` + // This error site throws a plain coded Error; tests run the development + // build, whose message names the rule. + if ( + error instanceof Error && + /must be at the top level of select\(\)/.test(error.message) + ) + return `nestedInclude` + return `other` +} + function groupKeyOf( row: Comment, grouping: Grouping, @@ -431,7 +510,17 @@ const shape = (include: boolean, grouping: Grouping): fc.Arbitrary => ), { nil: undefined, freq: 5 }, ), - include: fc.boolean().map((b) => (b ? true : undefined)), + include: fc.option( + fc.record({ + wrap: fc.constantFrom(`toArray` as const, `bare` as const), + at: fc.constantFrom( + `field` as const, + `nested` as const, + `case` as const, + ), + }), + { nil: undefined, freq: 2 }, + ), having: fc.option( fc.record({ // A condition may read aggregates, selected as fields or not. One @@ -541,13 +630,24 @@ function buildSelect( const fields: Record = Object.fromEntries( Object.entries(s.fields).map(([key, p]) => [key, build(p, c, issue)]), ) - if (s.include) - fields.nestedKids = toArray( - q + if (s.include) { + const { wrap, at } = s.include + // Each use builds its own child query, as separate select values do. + const kids = () => { + const child = q .from({ i: issueSource }) .where(({ i }: any) => eq(i.k, c.k)) - .select(({ i }: any) => ({ id: i.id })), - ) + .select(({ i }: any) => ({ id: i.id })) + return wrap === `toArray` ? toArray(child) : child + } + // A constant condition, so the conditional reads no source field. + fields.nestedKids = + at === `field` + ? kids() + : at === `nested` + ? { inner: kids() } + : caseWhen(eq(1, 1), { inner: kids() }, { inner: kids() }) + } if (s.spread === `source`) return { ...c, ...fields } if (s.spread === `parent`) return { ...issue, ...fields } return fields @@ -560,8 +660,22 @@ async function runHistory( shadow: boolean, steps: ReadonlyArray, union = false, + bareKeys = false, ): Promise { const grouped = grouping !== `none` + reach?.beginHistory() + for (const s of [top, nested]) { + if (s.include?.wrap === `bare`) + reach?.reach(`bare include: ${s.include.at}`) + if (s.include?.wrap === `toArray` && s.include.at === `case`) + reach?.reach(`toArray include in a conditional branch`) + if (grouped && bareKeys) + reach?.reach( + shapeHasAggregate(s) + ? `bare group keys beside an aggregate` + : `bare group keys without an aggregate`, + ) + } // Each use builds a new list literal, as separate query callbacks do. keyExpression = grouping === `upper` @@ -613,12 +727,11 @@ async function runHistory( // alias. const rowOf = (refs: any, alias: string | undefined) => alias === undefined ? refs : refs[alias] - const group = (query: any, alias: string | undefined) => - grouped - ? query.groupBy( - groupKeys((refs: any) => keyExpression(rowOf(refs, alias))), - ) - : query + const group = (query: any, alias: string | undefined) => { + if (!grouped) return query + const keys = (refs: any) => keyExpression(rowOf(refs, alias)) + return query.groupBy(bareKeys ? keys : groupKeys(keys)) + } const having = ( s: Shape, query: any, @@ -691,18 +804,17 @@ async function runHistory( [`top`, topQuery, top], [`include`, includeQuery, nested], ] as const) { - const error = modelError(s, grouping) + const errors = modelErrors(s, grouping, bareKeys) // A nested include cannot correlate with a unionAll row, which has no // source alias, so the builder rejects it first. - const unionInclude = label === `top` && union && s.include === true - if (error) { + const unionInclude = label === `top` && union && s.include !== undefined + if (errors.size > 0) { expect(result.ok, `${label} compile throws`).toBe(false) if (!result.ok && !unionInclude) - expect(result.error, `${label} error class`).toBeInstanceOf( - error === `nonAggregate` - ? NonAggregateExpressionNotInGroupByError - : AggregateFunctionNotInSelectError, - ) + expect( + [...errors], + `${label} error class (${String(result.error)})`, + ).toContain(errorName(result.error)) } else { expect(result.ok, `${label} compiles`).toBe(true) } @@ -794,31 +906,50 @@ const history = fc fc.boolean(), fc.array(step, { maxLength: 6 }), fc.boolean(), + fc.boolean(), ), ) +/** The states the operator laws need; each campaign must reach all of them. */ +const reachStates = [ + `bare include: field`, + `bare include: nested`, + `bare include: case`, + `toArray include in a conditional branch`, + `bare group keys without an aggregate`, + `bare group keys beside an aggregate`, +] as const +let reach: + ReturnType> | undefined + describe(`aggregate select shapes`, () => { it(`match the model across generated shapes (fixed campaign)`, async () => { // A repeatable baseline. Seed 2094 is arbitrary. + reach = createOracleReach(`${PROPERTY} (fixed)`, reachStates) await fc.assert( fc.asyncProperty( history, - ([top, nested, grouping, shadow, steps, union]) => - runHistory(top, nested, grouping, shadow, steps, union), + ([top, nested, grouping, shadow, steps, union, bareKeys]) => + runHistory(top, nested, grouping, shadow, steps, union, bareKeys), ), { numRuns: oracleRuns(120), seed: 2094 }, ) + reach.assertReached() + reach = undefined }) it(`match the model across generated shapes (random or replayed)`, async () => { + reach = createOracleReach(`${PROPERTY} (random)`, reachStates) await fc.assert( fc.asyncProperty( history, - ([top, nested, grouping, shadow, steps, union]) => - runHistory(top, nested, grouping, shadow, steps, union), + ([top, nested, grouping, shadow, steps, union, bareKeys]) => + runHistory(top, nested, grouping, shadow, steps, union, bareKeys), ), oraclePropertyOptions(120, PROPERTY), ) + reach.assertReached() + reach = undefined }) // Pinned replay of a review counterexample: a grouped select value that diff --git a/packages/db/tests/query/hand-built-ir-operators.test.ts b/packages/db/tests/query/hand-built-ir-operators.test.ts new file mode 100644 index 0000000000..8a5a322a1a --- /dev/null +++ b/packages/db/tests/query/hand-built-ir-operators.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { + Query, + count, + createCollection, + groupingCompiler, + include, + includesRuntime, +} from '../../src/index.js' +import { mockSyncCollectionOptions } from '../utils.js' + +// Code that builds query IR by hand passes operator code to `IR.Aggregate` +// and `IR.IncludesSubquery`. The public entry exports that code, and it must +// be the same value the operator functions carry, so hand-built and +// builder-built IR compile the same way. +describe(`operator code for hand-built IR`, () => { + it(`exports the grouping compiler that aggregates carry`, () => { + expect(groupingCompiler).toBe(count(1).grouping) + }) + + it(`exports the include runtime that include() carries`, () => { + const issues = createCollection( + mockSyncCollectionOptions<{ id: number }>({ + id: `hand-built-ir-issues`, + getKey: (issue) => issue.id, + initialData: [], + }), + ) + expect(includesRuntime).toBe( + include(new Query().from({ i: issues })).runtime, + ) + }) +}) diff --git a/packages/db/tests/query/join.test-d.ts b/packages/db/tests/query/join.test-d.ts index 03c8f49899..b8cf6e5176 100644 --- a/packages/db/tests/query/join.test-d.ts +++ b/packages/db/tests/query/join.test-d.ts @@ -182,12 +182,12 @@ describe(`Join Types - Type Safety`, () => { .join( { dept: departmentsCollection }, ({ user, dept }) => eq(user.department_id, dept.id), - `left`, + `left`, // dept is optional ) .join( { project: projectsCollection }, ({ user, project }) => eq(user.id, project.user_id), - `right`, + `right`, // user becomes optional, project required ), }) @@ -543,8 +543,9 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin({ user: userCollection }, ({ event, user }) => - eq(event.user_id, user.id), + .innerJoin( + { user: userCollection }, + ({ event, user }) => eq(event.user_id, user.id), // Should work with optional field ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -557,8 +558,9 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin({ user: userCollection }, ({ event, user }) => - eq(user.id, event.user_id), + .innerJoin( + { user: userCollection }, + ({ event, user }) => eq(user.id, event.user_id), // Swapped argument order ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -630,8 +632,9 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin({ user: userCollection }, ({ event, user }) => - eq(event.user_id, user.id), + .innerJoin( + { user: userCollection }, + ({ event, user }) => eq(event.user_id, user.id), // Should work with optional field ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -644,8 +647,9 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin({ user: userCollection }, ({ event, user }) => - eq(user.id, event.user_id), + .innerJoin( + { user: userCollection }, + ({ event, user }) => eq(user.id, event.user_id), // Swapped argument order ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -717,8 +721,9 @@ describe(`Join Alias Methods - Type Safety`, () => { query: (q) => q .from({ post: postCollection }) - .leftJoin({ user: userCollection }, ({ post, user }) => - eq(post.author_id, user.id), + .leftJoin( + { user: userCollection }, + ({ post, user }) => eq(post.author_id, user.id), // Should work with nullable field ) .select(({ post, user }) => ({ postTitle: post.title, @@ -1031,8 +1036,9 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin({ user: userCollection }, ({ event, user }) => - eq(event.user_id, user.id), + .innerJoin( + { user: userCollection }, + ({ event, user }) => eq(event.user_id, user.id), // Should work with optional field ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -1045,8 +1051,9 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ event: eventCollection }) - .innerJoin({ user: userCollection }, ({ event, user }) => - eq(user.id, event.user_id), + .innerJoin( + { user: userCollection }, + ({ event, user }) => eq(user.id, event.user_id), // Swapped argument order ) .select(({ event, user }) => ({ eventTitle: event.title, @@ -1118,8 +1125,9 @@ describe(`Join with ArkType Schemas`, () => { query: (q) => q .from({ post: postCollection }) - .leftJoin({ user: userCollection }, ({ post, user }) => - eq(post.author_id, user.id), + .leftJoin( + { user: userCollection }, + ({ post, user }) => eq(post.author_id, user.id), // Should work with nullable field ) .select(({ post, user }) => ({ postTitle: post.title, diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index 25292506e8..025b5e2e8d 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -375,7 +375,7 @@ export function warnUnhashableDerivedIdentity( * q.from({ issues: issueCollection }) * .join({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) - * )) + * ) * .select(({ issues, persons }) => ({ * id: issues.id, * title: issues.title, diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index ec53cdb791..f8e8d7b512 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -65,7 +65,7 @@ type InferConditionalResultType = * q.from({ issues: issueCollection }) * .join({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) - * )) + * ) * .select(({ issues, persons }) => ({ * id: issues.id, * title: issues.title, diff --git a/packages/svelte-db/skills/svelte-db/SKILL.md b/packages/svelte-db/skills/svelte-db/SKILL.md index ec92d3a161..ac2ababcba 100644 --- a/packages/svelte-db/skills/svelte-db/SKILL.md +++ b/packages/svelte-db/skills/svelte-db/SKILL.md @@ -118,17 +118,19 @@ When a query uses `include()` for a subquery in `select`, each child field is a ```svelte diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index a5c89be708..e7a1e23a26 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -179,7 +179,7 @@ function toValue(value: MaybeGetter): T { * q.from({ issues: issueCollection }) * .join({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) - * )) + * ) * .select(({ issues, persons }) => ({ * id: issues.id, * title: issues.title, diff --git a/packages/vue-db/skills/vue-db/SKILL.md b/packages/vue-db/skills/vue-db/SKILL.md index 5fe6f8c1f6..61dcbbaea8 100644 --- a/packages/vue-db/skills/vue-db/SKILL.md +++ b/packages/vue-db/skills/vue-db/SKILL.md @@ -118,16 +118,18 @@ When a query uses `include()` for a subquery in `select`, each child field is a ```vue diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 72119dae51..2cc1d3bf8b 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -152,7 +152,7 @@ export interface UseLiveQueryReturnWithSingleResultCollection< * q.from({ issues: issueCollection }) * .join({ persons: personCollection }, ({ issues, persons }) => * eq(issues.userId, persons.id) - * )) + * ) * .select(({ issues, persons }) => ({ * id: issues.id, * title: issues.title, diff --git a/scripts/check-doc-snippets.mjs b/scripts/check-doc-snippets.mjs index 8bcc4c927c..4d8a9c0fa6 100644 --- a/scripts/check-doc-snippets.mjs +++ b/scripts/check-doc-snippets.mjs @@ -26,6 +26,7 @@ import { fileURLToPath } from 'node:url' import prettier from 'prettier' import ts from 'typescript' import { + componentScript, fingerprint, misspelling, parses, @@ -89,21 +90,30 @@ for (const pkg of readdirSync(join(root, `packages`))) { // A fence closes with as many backticks as it opened with, so a ````ts // example can hold a ``` block. -const fence = /^(`{3,})(ts|tsx|typescript)([^\n]*)\n([\s\S]*?)^\1[ \t]*$/gm +const fence = + /^(`{3,})(ts|tsx|typescript|svelte|vue)([^\n]*)\n([\s\S]*?)^\1[ \t]*$/gm const snippets = [] for (const file of markdown) { const text = readFileSync(file, `utf8`) for (const match of text.matchAll(fence)) { if (match[3].includes(`nocheck`)) continue - const line = text.slice(0, match.index).split(`\n`).length + let line = text.slice(0, match.index).split(`\n`).length + let code = match[4] + // A component example: check its TypeScript script, not its template. + if (match[2] === `svelte` || match[2] === `vue`) { + const script = componentScript(code) + if (!script) continue + code = script.code + line += script.offset + } snippets.push({ file: relative(root, file), line, - code: match[4], + code, name: join( root, `.snippet-check`, - `${snippets.length}.${/<\/|<[A-Z][A-Za-z]*[ />]/.test(match[4]) ? `tsx` : `ts`}`, + `${snippets.length}.${/<\/|<[A-Z][A-Za-z]*[ />]/.test(code) ? `tsx` : `ts`}`, ), }) } diff --git a/scripts/doc-snippet-syntax.mjs b/scripts/doc-snippet-syntax.mjs index f49ffd5819..d830251643 100644 --- a/scripts/doc-snippet-syntax.mjs +++ b/scripts/doc-snippet-syntax.mjs @@ -19,10 +19,10 @@ const isJsx = (node) => /** * The tokens of an example, without what formatting changes, so a reformatted - * example keeps its baseline entry: spacing, comments, commas, semicolons, - * the parentheses around multi-line JSX, and the parentheses around a single - * arrow parameter. Other parentheses stay, because moving one changes what - * the code calls. The tree, not a token scan, finds JSX: a parenthesis in JSX + * example keeps its baseline entry: spacing, comments, trailing commas, + * semicolons, the parentheses around multi-line JSX, and the parentheses + * around a single arrow parameter. Other commas and parentheses stay, because + * moving one changes what the code calls. The tree, not a token scan, finds JSX: a parenthesis in JSX * text is text. */ export function fingerprint(code, jsx) { @@ -33,7 +33,6 @@ export function fingerprint(code, jsx) { const children = node.getChildren() if (children.length === 0) { if ( - node.kind === ts.SyntaxKind.CommaToken || node.kind === ts.SyntaxKind.SemicolonToken || node.kind === ts.SyntaxKind.EndOfFileToken ) @@ -51,13 +50,16 @@ export function fingerprint(code, jsx) { node.parameters.length === 1 && ts.isIdentifier(node.parameters[0].name) && !node.parameters[0].type - for (const child of children) - if (!( + children.forEach((child, index) => { + const parameterParen = bareParameter && (child.kind === ts.SyntaxKind.OpenParenToken || child.kind === ts.SyntaxKind.CloseParenToken) - )) - visit(child) + // A trailing comma ends its list; other commas separate values. + const trailingComma = + child.kind === ts.SyntaxKind.CommaToken && index === children.length - 1 + if (!parameterParen && !trailingComma) visit(child) + }) } visit(parse(code, jsx)) return tokens.join(` `) @@ -126,10 +128,25 @@ export function misspelling(name, imported) { ) } +/** + * The TypeScript in a Svelte or Vue component example: its `\n

    {a}

    `, + ) + if (component?.code !== `const a = 1\n` || component.offset !== 2) + throw new Error( + `self-test: componentScript read ${JSON.stringify(component)}`, + ) + if (componentScript(``) !== undefined) + throw new Error( + `self-test: componentScript read a script without lang="ts"`, + ) + if (componentScript(``)?.code !== `x`) + throw new Error( + `self-test: componentScript missed