import { AndExpr, DerivedTableSource, JoinAst, type TableSource, } from '@prisma-next/sql-relational-core/ast'; import type { AggregateFunctions, Expression, ExpressionBuilder, ExtractScopeFields, FieldProxy, WithField, WithFields, } from '../expression'; import type { EmptyRow, Expand, JoinOuterScope, JoinSource, MergeScopes, NullableScope, QueryContext, Scope, ScopeField, ScopeTable, Subquery, } from '../scope'; import type { JoinedTables } from '../types/joined-tables'; import type { SelectQuery } from '../types/select-query'; import type { LateralBuilder } from '../types/shared'; import { BuilderBase, type BuilderContext, type BuilderState, cloneState, emptyState, mergeScopes, nullableScope, resolveSelectArgs, } from './builder-base'; import { createFieldProxy } from './field-proxy'; import { createFunctions } from './functions'; import { SelectQueryImpl } from './query-impl'; export class JoinedTablesImpl extends BuilderBase implements JoinedTables { readonly #state: BuilderState; constructor(state: BuilderState, ctx: BuilderContext) { super(ctx); this.#state = state; } lateralJoin = this._gate( { sql: { lateral: true } }, 'lateralJoin', >( alias: Alias, builder: (lateral: LateralBuilder) => Subquery, ): JoinedTables< QC, MergeScopes }> > => { const { derivedSource, lateralScope } = this.#buildLateral(alias, builder); const resultScope = mergeScopes( this.#state.scope as AvailableScope, lateralScope as { topLevel: LateralRow; namespaces: Record }, ); return this.#addLateralJoin('inner', resultScope, derivedSource); }, ) as JoinedTables['lateralJoin']; outerLateralJoin = this._gate( { sql: { lateral: true } }, 'outerLateralJoin', >( alias: Alias, builder: (lateral: LateralBuilder) => Subquery, ): JoinedTables< QC, MergeScopes< AvailableScope, NullableScope<{ topLevel: LateralRow; namespaces: Record }> > > => { const { derivedSource, lateralScope } = this.#buildLateral(alias, builder); const resultScope = mergeScopes( this.#state.scope as AvailableScope, nullableScope( lateralScope as { topLevel: LateralRow; namespaces: Record }, ), ); return this.#addLateralJoin('left', resultScope, derivedSource); }, ) as JoinedTables['outerLateralJoin']; select( ...columns: Columns ): SelectQuery>; select( alias: Alias, expr: (fields: FieldProxy, fns: AggregateFunctions) => Expression, ): SelectQuery>; select>>( callback: (fields: FieldProxy, fns: AggregateFunctions) => Result, ): SelectQuery>>; select(...args: unknown[]): unknown { const { projections, newRowFields } = resolveSelectArgs(args, this.#state.scope, this.ctx); return new SelectQueryImpl( cloneState(this.#state, { projections: [...this.#state.projections, ...projections], rowFields: { ...this.#state.rowFields, ...newRowFields }, }), this.ctx, ); } innerJoin>( other: Other, on: ExpressionBuilder, QC>, ): JoinedTables> { const targetScope = mergeScopes( this.#state.scope as AvailableScope, other.getJoinOuterScope() as Other[typeof JoinOuterScope], ); return this.#addJoin(other, 'inner', targetScope, on); } outerLeftJoin>( other: Other, on: ExpressionBuilder, QC>, ): JoinedTables>> { const targetScope = mergeScopes( this.#state.scope as AvailableScope, nullableScope(other.getJoinOuterScope() as Other[typeof JoinOuterScope]), ); return this.#addJoin(other, 'left', targetScope, on); } outerRightJoin>( other: Other, on: ExpressionBuilder, QC>, ): JoinedTables, Other[typeof JoinOuterScope]>> { const targetScope = mergeScopes( nullableScope(this.#state.scope as AvailableScope), other.getJoinOuterScope() as Other[typeof JoinOuterScope], ); return this.#addJoin(other, 'right', targetScope, on); } outerFullJoin>( other: Other, on: ExpressionBuilder, QC>, ): JoinedTables< QC, MergeScopes, NullableScope> > { const targetScope = mergeScopes( nullableScope(this.#state.scope as AvailableScope), nullableScope(other.getJoinOuterScope() as Other[typeof JoinOuterScope]), ); return this.#addJoin(other, 'full', targetScope, on); } #addJoin, ResultScope extends Scope>( other: Other, joinType: 'inner' | 'left' | 'right' | 'full', resultScope: ResultScope, onExpr: ExpressionBuilder, QC>, ): JoinedTables { const fieldProxy = createFieldProxy( mergeScopes( this.#state.scope as AvailableScope, other.getJoinOuterScope() as Other[typeof JoinOuterScope], ), ) as FieldProxy>; const fns = createFunctions(this.ctx.queryOperationTypes, this.ctx.rawCodecInferer); const onResult = onExpr(fieldProxy, fns); const joinAst = new JoinAst(joinType, other.buildAst(), onResult.buildAst()); return new JoinedTablesImpl( cloneState(this.#state, { joins: [...this.#state.joins, joinAst], scope: resultScope, }), this.ctx, ); } #buildLateral( alias: string, builderFn: ( lateral: LateralBuilder, ) => Subquery>, ) { const lateralBuilder: LateralBuilder = { from: (other) => { const otherScope = other.getJoinOuterScope(); const parentMerged = mergeScopes(this.#state.scope, otherScope); return new SelectQueryImpl( emptyState(other.buildAst() as TableSource, parentMerged), this.ctx, ) as unknown as SelectQuery; }, }; const subquery = builderFn(lateralBuilder); const subqueryAst = subquery.buildAst(); const derivedSource = DerivedTableSource.as(alias, subqueryAst); const subqueryRowFields: ScopeTable = subquery.getRowFields(); const lateralScope: Scope = { topLevel: subqueryRowFields, namespaces: { [alias]: subqueryRowFields }, }; return { derivedSource, lateralScope }; } #addLateralJoin( joinType: 'inner' | 'left', resultScope: ResultScope, derivedSource: DerivedTableSource, ): JoinedTables { const onExpr = AndExpr.of([]); const joinAst = new JoinAst(joinType, derivedSource, onExpr, true); return new JoinedTablesImpl( cloneState(this.#state, { joins: [...this.#state.joins, joinAst], scope: resultScope, }), this.ctx, ); } }