import { Condition, SqlFragment, FieldRef } from './conditions'; import { TableSchema } from '../schema/table-builder'; import type { DatabaseClient } from '../database/database-client.interface'; import type { OrderDirection } from '../entity/db-context'; import { QueryExecutor } from '../entity/db-context'; import { Subquery } from './subquery'; import type { ManualJoinDefinition, JoinType } from './query-builder'; import { DbCte } from './cte-builder'; /** * Query context for tracking CTEs and parameters */ interface QueryContext { ctes: Map; cteCounter: number; paramCounter: number; allParams: any[]; /** True when the driver cannot decode native ARRAY result columns (BunClient). */ useJsonArrayAggregation?: boolean; } /** * Type helper to detect if a type is a class instance (has prototype methods) * vs a plain data object. See conditions.ts for detailed explanation. * Excludes DbColumn and SqlFragment which have valueOf but are not value types. */ type IsClassInstance = T extends { __isDbColumn: true; } ? false : T extends SqlFragment ? false : T extends { valueOf(): infer V; } ? V extends T ? true : V extends number | string | boolean | bigint | symbol ? true : false : false; /** * Check for types with known class method signatures */ type HasClassMethods = T extends { getTime(): number; } ? true : T extends { size: number; has(value: any): boolean; } ? true : T extends { byteLength: number; } ? true : T extends { then(onfulfilled?: any): any; } ? true : T extends { message: string; name: string; } ? true : T extends { exec(string: string): any; } ? true : false; /** * Combined check for value types that should not be recursively processed */ type IsValueType = IsClassInstance extends true ? true : HasClassMethods extends true ? true : false; /** * Type helper to resolve FieldRef types to their value types * Preserves class instances (Date, Map, Set, Temporal, etc.) as-is */ type ResolveFieldRefs = T extends FieldRef ? V : T extends Array ? Array> : T extends object ? IsValueType extends true ? T : { [K in keyof T]: ResolveFieldRefs; } : T; /** * Type helper to convert resolved value types back to FieldRef for join conditions * This allows join conditions to accept either the value or FieldRef * If a field is already a FieldRef, it preserves it without double-wrapping * Preserves class instances (Date, Map, Set, Temporal, etc.) as-is */ type ToFieldRefs = T extends object ? IsValueType extends true ? FieldRef : { [K in keyof T]: T[K] extends FieldRef ? FieldRef : FieldRef; } : T extends FieldRef ? FieldRef : FieldRef; /** * Represents a grouped item with access to the grouping key and aggregate functions * TGroupingKey: The shape of the grouping key (e.g., { street: string }) * TOriginalRow: The original row type before grouping */ export interface GroupedItem { /** * The grouping key - contains all fields specified in groupBy */ readonly key: ResolveFieldRefs; /** * Count the number of items in this group */ count(): number; /** * Sum a numeric field across all items in this group * Returns the inferred type from the selector, or number if the type cannot be inferred */ sum(selector: (item: TOriginalRow) => TField): TField extends FieldRef ? V : TField extends number ? number : number; /** * Get the minimum value of a field across all items in this group * Returns the inferred type from the selector */ min(selector: (item: TOriginalRow) => TField): TField extends FieldRef ? V : TField; /** * Get the maximum value of a field across all items in this group * Returns the inferred type from the selector */ max(selector: (item: TOriginalRow) => TField): TField extends FieldRef ? V : TField; /** * Get the average value of a numeric field across all items in this group * Always returns number since average is always numeric * Accepts undefined since SQL AVG ignores NULL values */ avg(selector: (item: TOriginalRow) => FieldRef | number | undefined): number; } /** * Aggregate field reference - used in HAVING clauses * Represents a field that is an aggregate function result (e.g., COUNT(*), SUM(column)) */ export interface AggregateFieldRef extends FieldRef { readonly __isAggregate: true; readonly __aggregateType: 'COUNT' | 'SUM' | 'MIN' | 'MAX' | 'AVG'; readonly __aggregateSelector?: (item: any) => any; } /** * Grouped query builder - result of calling groupBy() * Provides type-safe access to grouping keys and aggregate functions */ export declare class GroupedQueryBuilder { private schema; private client; private originalSelector; private groupingKeySelector; private whereCond?; private havingCond?; private limitValue?; private offsetValue?; private orderByFields; private executor?; private manualJoins; private joinCounter; private schemaRegistry?; constructor(schema: TableSchema, client: DatabaseClient, originalSelector: (row: any) => any, groupingKeySelector: (row: TOriginalRow) => TGroupingKey, whereCond?: Condition, executor?: QueryExecutor, manualJoins?: ManualJoinDefinition[], joinCounter?: number, schemaRegistry?: Map); /** * Override the timeout for this single query (ms). Only this query is wrapped * (`SET LOCAL statement_timeout`). Overrides the connection-level default; pass * `0` to disable. On timeout a `QueryTimeoutError` is thrown. */ withTimeout(timeoutMs: number): this; /** * Mark this query as expected to finish within `expectedMs` (ms). If it runs * longer, the context's `onQueryTakingTooLong` callback fires (the query is * NOT cancelled). Overrides the context's `longRunningQueryThreshold`. */ expectedExecutionTime(expectedMs: number): this; /** * Select from grouped results * The selector receives a GroupedItem with key and aggregate functions */ select(selector: (group: GroupedItem) => TSelection): GroupedSelectQueryBuilder; /** * Add HAVING condition (filter groups after aggregation) */ having(condition: (group: GroupedItem) => Condition): this; /** * Create a mock GroupedItem for type inference and condition building */ private createMockGroupedItem; /** * Create mock row for the original table */ private createMockRow; } /** * Grouped select query builder - result of calling select() on a GroupedQueryBuilder */ export declare class GroupedSelectQueryBuilder { private schema; private client; private originalSelector; private groupingKeySelector; private resultSelector; private whereCond?; private havingCond?; private limitValue?; private offsetValue?; private orderByFields; private executor?; private manualJoins; private joinCounter; private schemaRegistry?; constructor(schema: TableSchema, client: DatabaseClient, originalSelector: (row: any) => any, groupingKeySelector: (row: TOriginalRow) => TGroupingKey, resultSelector: (group: GroupedItem) => TSelection, whereCond?: Condition, havingCond?: Condition, limit?: number, offset?: number, orderBy?: Array<{ field: string; direction: 'ASC' | 'DESC'; }>, executor?: QueryExecutor, manualJoins?: ManualJoinDefinition[], joinCounter?: number, schemaRegistry?: Map); /** * Override the timeout for this single query (ms). Only this query is wrapped * (`SET LOCAL statement_timeout`). Overrides the connection-level default; pass * `0` to disable. On timeout a `QueryTimeoutError` is thrown. */ withTimeout(timeoutMs: number): this; /** * Mark this query as expected to finish within `expectedMs` (ms). If it runs * longer, the context's `onQueryTakingTooLong` callback fires (the query is * NOT cancelled). Overrides the context's `longRunningQueryThreshold`. */ expectedExecutionTime(expectedMs: number): this; /** * Add HAVING condition (filter groups after aggregation) */ having(condition: (group: GroupedItem) => Condition): this; /** * Limit results */ limit(count: number): this; /** * Offset results */ offset(count: number): this; /** * Order by field(s) from the selected result * @example * .orderBy(p => p.colName) * .orderBy(p => [p.colName, p.otherCol]) * .orderBy(p => [[p.colName, 'ASC'], [p.otherCol, 'DESC']]) */ orderBy(selector: (row: TSelection) => T): this; orderBy(selector: (row: TSelection) => T[]): this; orderBy(selector: (row: TSelection) => Array<[T, OrderDirection]>): this; /** * Execute query and return results */ toList(): Promise[]>; /** * Transform database results - convert aggregate values and apply mappers */ private transformResults; /** * Get selection metadata for mapper preservation in CTEs * Enhances the selection result with mapper info from original schema columns * @internal */ getSelectionMetadata(): Record; /** * Execute query and return first result or null */ first(): Promise | null>; /** * Execute query and return first result or null (alias for first) */ firstOrDefault(): Promise | null>; /** * Execute query and return first result or throw */ firstOrThrow(): Promise>; /** * Convert to subquery for use in other queries */ asSubquery(mode?: TMode): Subquery : TMode extends 'array' ? ResolveFieldRefs[] : ResolveFieldRefs, TMode>; /** * Build SQL for use in CTEs - public interface for CTE builder * @internal */ buildCteQuery(queryContext: QueryContext): { sql: string; params: any[]; }; /** * Add a LEFT JOIN to the grouped query result * This wraps the grouped query as a subquery and joins to it * * @example * const result = await db.orders * .select(o => ({ customerId: o.customerId, total: o.total })) * .groupBy(o => ({ customerId: o.customerId })) * .select(g => ({ customerId: g.key.customerId, totalSum: g.sum(o => o.total) })) * .leftJoin( * customerDetailsCte, * (grouped, details) => eq(grouped.customerId, details.customerId), * (grouped, details) => ({ ...grouped, details: details.items }) * ) * .toList(); */ leftJoin, TNewSelection>(rightSource: Subquery | DbCte, condition: (left: ToFieldRefs, right: ToFieldRefs) => Condition, selector: (left: ToFieldRefs, right: ToFieldRefs) => TNewSelection, alias?: string): GroupedJoinedQueryBuilder, ToFieldRefs>; /** * Add an INNER JOIN to the grouped query result * This wraps the grouped query as a subquery and joins to it */ innerJoin, TNewSelection>(rightSource: Subquery | DbCte, condition: (left: ToFieldRefs, right: ToFieldRefs) => Condition, selector: (left: ToFieldRefs, right: ToFieldRefs) => TNewSelection, alias?: string): GroupedJoinedQueryBuilder, ToFieldRefs>; /** * Internal join implementation */ private joinInternal; /** * Create a mock object for the current selection (for join conditions) * The key is the alias used in the SELECT clause, so we use it as __dbColumnName */ private createMockForSelection; /** * Create a mock for a subquery result */ private createMockForSubquery; /** * Create a mock for a CTE */ private createMockForCte; /** * Build the SQL query for grouped results * * Optimization: When grouping by SqlFragment expressions, we wrap the base query * in a subquery to avoid repeating complex expressions in both SELECT and GROUP BY. * This improves query performance by computing expressions only once. * * Without optimization: * SELECT complex_expr as "alias" FROM table GROUP BY complex_expr * * With optimization: * SELECT "alias" FROM (SELECT complex_expr as "alias" FROM table) q1 GROUP BY "alias" */ private buildQuery; /** * Build a simple grouped query when GROUP BY only contains column references */ private buildSimpleGroupedQuery; /** * Build a grouped query with subquery wrapping for complex GROUP BY expressions * This avoids repeating SqlFragment expressions in both SELECT and GROUP BY */ private buildQueryWithSubqueryWrapping; /** * Build a single SELECT part for a result field */ private buildSelectPart; /** * Create mock row for the original table */ private createMockRow; /** * Create a mock GroupedItem for type inference */ private createMockGroupedItem; /** * Detect navigation property references in a WHERE condition and add necessary JOINs */ private detectAndAddJoinsFromCondition; /** * Detect navigation properties in a selection and add JOINs for them */ private detectAndAddJoinsFromSelection; /** * Collect all table aliases from a selection */ private collectTableAliasesFromSelection; /** * Resolve all navigation joins by finding the correct path through the schema graph. * Handles multi-level navigation like task.level.createdBy. */ private resolveJoinsForTableAliases; /** * Build HAVING condition SQL - handles aggregate field refs specially */ private buildHavingCondition; /** * Transform a HAVING condition by replacing aggregate field refs with SQL fragments */ private transformHavingCondition; /** * Build SQL for an aggregate field reference */ private buildAggregateFieldSql; /** * Get the operator string from a condition object */ private getOperatorForCondition; } /** * Query builder for grouped queries that have been joined * This handles the case where a GroupedSelectQueryBuilder is joined with a CTE or subquery */ export declare class GroupedJoinedQueryBuilder { private schema; private client; private leftSubquery; private leftAlias; private rightSource; private rightAlias; private joinType; private joinCondition; private resultSelector; private createLeftMock; private createRightMock; private executor?; private cte?; private limitValue?; private offsetValue?; private orderByFields; private additionalJoins; constructor(schema: TableSchema, client: DatabaseClient, leftSubquery: Subquery, leftAlias: string, rightSource: Subquery | DbCte, rightAlias: string, joinType: JoinType, joinCondition: Condition, resultSelector: (left: TLeft, right: TRight) => TSelection, createLeftMock: () => TLeft, createRightMock: () => TRight, executor?: QueryExecutor, cte?: DbCte); /** * Override the timeout for this single query (ms). Only this query is wrapped * (`SET LOCAL statement_timeout`). Overrides the connection-level default; pass * `0` to disable. On timeout a `QueryTimeoutError` is thrown. */ withTimeout(timeoutMs: number): this; /** * Mark this query as expected to finish within `expectedMs` (ms). If it runs * longer, the context's `onQueryTakingTooLong` callback fires (the query is * NOT cancelled). Overrides the context's `longRunningQueryThreshold`. */ expectedExecutionTime(expectedMs: number): this; /** * Limit results */ limit(count: number): this; /** * Offset results */ offset(count: number): this; /** * Order by field(s) from the selected result */ orderBy(selector: (row: TSelection) => T): this; orderBy(selector: (row: TSelection) => T[]): this; orderBy(selector: (row: TSelection) => Array<[T, OrderDirection]>): this; /** * Execute query and return results */ toList(): Promise[]>; /** * Execute query and return first result or null */ first(): Promise | null>; /** * Execute query and return first result or null (alias for first) */ firstOrDefault(): Promise | null>; /** * Execute query and return first result or throw */ firstOrThrow(): Promise>; /** * Convert to subquery for use in other queries */ asSubquery(mode?: TMode): Subquery : TMode extends 'array' ? ResolveFieldRefs[] : ResolveFieldRefs, TMode>; /** * Get CTEs used by this query builder * @internal */ getReferencedCtes(): DbCte[]; /** * Get selection metadata for mapper preservation in CTEs * Enhances the selection result with mapper info from the left subquery * @internal */ getSelectionMetadata(): Record; /** * Build SQL for use in CTEs - public interface for CTE builder * This returns SQL WITHOUT the WITH clause - CTEs should be extracted separately via getReferencedCtes() * @internal */ buildCteQuery(queryContext: QueryContext): { sql: string; params: any[]; }; /** * Build the SQL query * @param skipCteClause If true, don't include WITH clause (for embedding in outer CTEs) */ private buildQuery; } export {}; //# sourceMappingURL=grouped-query.d.ts.map