import { DatabaseClient } from '../database/database-client.interface'; import { QueryContext } from './query-builder'; /** * Represents a field in a collection selection that can be either a simple expression * or a nested object structure */ export interface SelectedField { alias: string; expression?: string; nested?: SelectedField[]; /** * The original property name from the schema (for mapper lookup). * When alias differs from the schema property name, this allows * the transformation to find the correct mapper. */ propertyName?: string; /** * The source table name for this field (for navigation properties). * When a field comes from a navigation join (e.g., i.userEshop.birthdate), * this specifies which table's schema to use for mapper lookup. */ sourceTable?: string; /** * When this field is a nested CTE (collection within collection), * this specifies how to join that CTE. */ nestedCteJoin?: { cteName: string; joinClause: string; }; /** * When this field is a nested collection (CollectionQueryBuilder), * this stores info needed for recursive mapper transformation. * Contains the nested collection's target table and selected field configs. */ nestedCollectionInfo?: { targetTable: string; selectedFieldConfigs?: SelectedField[]; isSingleResult?: boolean; flattenResultType?: 'number' | 'string'; }; } /** * Collection aggregation strategy type */ export type CollectionStrategyType = 'cte' | 'temptable' | 'lateral'; /** * Result of building a collection aggregation */ export interface CollectionAggregationResult { /** * SQL for the CTE or temp table creation */ sql: string; /** * Parameters for the SQL query */ params: any[]; /** * The CTE name or temp table name to join against */ tableName: string; /** * Join clause to connect the aggregation to the main query * Example: 'LEFT JOIN "cte_0" ON "users"."id" = "cte_0".parent_id' */ joinClause: string; /** * Column expression to select from the joined aggregation * Example: 'COALESCE("cte_0".data, \'[]\'::jsonb)' */ selectExpression: string; /** * Whether this is a CTE (goes in WITH clause) or requires separate execution */ isCTE: boolean; /** * Optional cleanup SQL (for temp tables) */ cleanupSql?: string; /** * If true, the data has been fetched and is available in `data` field * (multi-statement query optimization) */ dataFetched?: boolean; /** * The fetched data (when dataFetched is true) * Map of parent_id -> aggregated data */ data?: Map; } /** * Navigation join information for multi-level navigation in collections */ export interface NavigationJoin { /** * Alias for the joined table (usually the relation name) */ alias: string; /** * The actual table name to join */ targetTable: string; /** * Schema name if different from public */ targetSchema?: string; /** * Foreign key columns in the source table */ foreignKeys: string[]; /** * Primary key columns in the target table to match */ matches: string[]; /** * Whether this is an INNER JOIN (true) or LEFT JOIN (false) */ isMandatory: boolean; /** * The source table alias for this join (the table that has the FK) */ sourceAlias: string; } /** * Configuration for building a collection aggregation */ export interface CollectionAggregationConfig { /** * Name of the relation (for naming CTEs/temp tables) */ relationName: string; /** * True when the driver cannot decode native ARRAY result columns * (BunClient) — 'array' aggregations must emit json_agg instead of * array_agg so no native array reaches the wire. */ useJsonArrayAggregation?: boolean; /** * Target table to query */ targetTable: string; /** * Foreign key column in target table. * @deprecated Use `foreignKeys` for full composite-/literal-predicate support. * Retained for backward compatibility; equals `foreignKeys[0]`. */ foreignKey: string; /** * Full FK column list on the target (child) side of the navigation. May * contain literal markers (`__LIT:`) for constant FK predicates such * as `withForeignKey: [col, isCurrent], withPrincipalKey: [id, true]` — the * literal pair becomes an AND-predicate (e.g. `target.is_current = TRUE`). * * When present, all collection strategies must build the parent-correlation * WHERE clause by iterating `foreignKeys` × `matches` (using `formatJoinValue`), * NOT by emitting a single `foreignKey = sourceTable.id` clause. The latter * silently drops every constant pair past index 0. * * Optional for backward compatibility. When undefined, strategies fall back * to the legacy single-column behavior (`foreignKey = sourceTable.id`). */ foreignKeys?: string[]; /** * Full match-key column list on the source (parent) side of the navigation. * Paired index-for-index with `foreignKeys`. May contain `__LIT:` * markers for literal principal-key constants. Defaults to `['id']` when * undefined. */ matches?: string[]; /** * Source table (parent) */ sourceTable: string; /** * Parent IDs to filter by (for temp table strategy) */ parentIds?: any[]; /** * Fields to select (supports nested object structures) */ selectedFields: SelectedField[]; /** * WHERE clause SQL (without WHERE keyword) */ whereClause?: string; /** * Parameters for WHERE clause */ whereParams?: any[]; /** * ORDER BY clause SQL (without ORDER BY keyword) - uses database column names * Used for ordering in subqueries that access raw table columns */ orderByClause?: string; /** * ORDER BY clause SQL using property names (aliases) * Used for ordering in json_agg which operates on aliased subquery output */ orderByClauseAlias?: string; /** * ORDER BY fields as individual entries (database column names and directions) * Used to include ORDER BY columns in inner SELECT for window functions */ orderByFields?: Array<{ field: string; direction: 'ASC' | 'DESC'; }>; /** * LIMIT value */ limitValue?: number; /** * OFFSET value */ offsetValue?: number; /** * Whether to use DISTINCT */ isDistinct?: boolean; /** * Whether this is a single result (firstOrDefault) instead of a list. * When true, returns a single JSON object instead of an array. * Uses row_to_json instead of json_agg. */ isSingleResult?: boolean; /** * Aggregation type */ aggregationType: 'jsonb' | 'array' | 'count' | 'min' | 'max' | 'sum' | 'exists'; /** * When the foreign key column is on a different table than the target table * (e.g., after selectMany flattening), this specifies which table alias * to use when qualifying the FK column. * Example: selectMany flattens product_price_capacity_groups through product_prices, * so FK "product_id" is on "product_prices", not on the target table. */ foreignKeyTableAlias?: string; /** * For scalar aggregations, the field to aggregate */ aggregateField?: string; /** * Pre-built SQL expression used as the aggregate argument instead of a column. * Set when the user's .sum/.min/.max selector returns a nested collection * (e.g. `.sum(row => other.where(...).count())`) — the strategy wraps this * expression with the aggregate function (`SUM()`) rather than * referencing a simple column. */ aggregateExpression?: string; /** * For array aggregations, the field to collect */ arrayField?: string; /** * Default value for empty aggregations */ defaultValue: string; /** * Counter for naming CTEs/temp tables */ counter: number; /** * Navigation joins needed for multi-level navigation in collection selectors. * These represent JOINs to related tables (e.g., task.level.createdBy). * This includes BOTH: * - Navigation path joins (for LATERAL correlation with outer query) * - Selector joins (for joins within the collection's own selector) * Use selectorNavigationJoins for just the selector joins (needed for CTE strategy). */ navigationJoins?: NavigationJoin[]; /** * Navigation joins detected from the collection's selector only. * Unlike navigationJoins, this does NOT include the navigation path from outer query. * CTE strategy should use this instead of navigationJoins since CTEs don't need * correlation with outer query tables (they join via parent_id). */ selectorNavigationJoins?: NavigationJoin[]; } /** * Strategy interface for collection aggregation */ export interface ICollectionStrategy { /** * Build the aggregation query (CTE or temp table) */ buildAggregation(config: CollectionAggregationConfig, context: QueryContext, client: DatabaseClient): Promise | CollectionAggregationResult; /** * Get the strategy type */ getType(): CollectionStrategyType; /** * Whether this strategy requires pre-execution of parent query * (temp table needs parent IDs first) */ requiresParentIds(): boolean; } //# sourceMappingURL=collection-strategy.interface.d.ts.map