drizzle-cube 0.3.32 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/adapters/express/index.cjs +1 -1
  2. package/dist/adapters/express/index.js +1 -1
  3. package/dist/adapters/fastify/index.cjs +1 -1
  4. package/dist/adapters/fastify/index.js +1 -1
  5. package/dist/adapters/hono/index.cjs +1 -1
  6. package/dist/adapters/hono/index.js +1 -1
  7. package/dist/adapters/{mcp-transport-B2rGcu1X.js → mcp-transport-Bbz3qrIy.js} +4324 -3230
  8. package/dist/adapters/mcp-transport-CXF4E5QJ.cjs +257 -0
  9. package/dist/adapters/nextjs/index.cjs +1 -1
  10. package/dist/adapters/nextjs/index.js +1 -1
  11. package/dist/adapters/utils.cjs +8 -8
  12. package/dist/adapters/utils.d.ts +2 -137
  13. package/dist/adapters/utils.js +1347 -1356
  14. package/dist/client/chunks/{DashboardEditModal-rLcmZpe_.js → DashboardEditModal-Bv7e3Q7O.js} +2 -2
  15. package/dist/client/chunks/{DashboardEditModal-rLcmZpe_.js.map → DashboardEditModal-Bv7e3Q7O.js.map} +1 -1
  16. package/dist/client/chunks/{analysis-builder-DCt5C58c.js → analysis-builder-BfH-w92z.js} +2962 -3019
  17. package/dist/client/chunks/analysis-builder-BfH-w92z.js.map +1 -0
  18. package/dist/client/chunks/{analysis-builder-shared-ysrRYGiU.js → analysis-builder-shared-DsbdRCzz.js} +361 -361
  19. package/dist/client/chunks/{analysis-builder-shared-ysrRYGiU.js.map → analysis-builder-shared-DsbdRCzz.js.map} +1 -1
  20. package/dist/client/chunks/useDirtyStateTracking-CTS_m9mg.js.map +1 -1
  21. package/dist/client/components/AnalysisBuilder/AnalysisResultsPanel.d.ts +2 -1
  22. package/dist/client/components/AnalysisBuilder/types.d.ts +18 -6
  23. package/dist/client/components.js +1 -1
  24. package/dist/client/hooks/queries/useDryRunQuery.d.ts +14 -48
  25. package/dist/client/hooks/useAnalysisBuilderHook.d.ts +6 -33
  26. package/dist/client/hooks/useAnalysisQueryExecution.d.ts +7 -7
  27. package/dist/client/hooks.js +194 -22
  28. package/dist/client/hooks.js.map +1 -1
  29. package/dist/client/index.js +3 -3
  30. package/dist/client/shared/types.d.ts +27 -0
  31. package/dist/client/styles.css +1 -1
  32. package/dist/client-bundle-stats.html +1 -1
  33. package/dist/server/index.cjs +49 -49
  34. package/dist/server/index.d.ts +1727 -547
  35. package/dist/server/index.js +4836 -3715
  36. package/package.json +1 -1
  37. package/dist/adapters/mcp-transport-DeD7YevT.cjs +0 -257
  38. package/dist/client/chunks/analysis-builder-DCt5C58c.js.map +0 -1
  39. package/dist/client/chunks/hooks-CdyIO1-j.js +0 -236
  40. package/dist/client/chunks/hooks-CdyIO1-j.js.map +0 -1
@@ -277,6 +277,151 @@ export declare interface CacheProvider {
277
277
  close?(): Promise<void>;
278
278
  }
279
279
 
280
+ /**
281
+ * Calculated Measure Resolver
282
+ * Manages dependency resolution for calculated measures
283
+ */
284
+ export declare class CalculatedMeasureResolver {
285
+ private dependencyGraph;
286
+ private cubes;
287
+ constructor(cubes: Map<string, Cube> | Cube);
288
+ /**
289
+ * Extract {member} references from calculatedSql template
290
+ * Supports both {measure} and {Cube.measure} syntax
291
+ *
292
+ * @param calculatedSql - Template string with {member} references
293
+ * @returns Array of dependency information
294
+ */
295
+ extractDependencies(calculatedSql: string): MeasureDependency[];
296
+ /**
297
+ * Build dependency graph for all calculated measures in a cube
298
+ *
299
+ * @param cube - The cube containing measures
300
+ */
301
+ buildGraph(cube: Cube): void;
302
+ /**
303
+ * Build dependency graph for multiple cubes
304
+ *
305
+ * @param cubes - Map of cubes to analyze
306
+ */
307
+ buildGraphForMultipleCubes(cubes: Map<string, Cube>): void;
308
+ /**
309
+ * Calculate in-degree for each node (number of measures depending on it)
310
+ */
311
+ private calculateInDegrees;
312
+ /**
313
+ * Perform topological sort using Kahn's algorithm
314
+ * Returns measures in dependency order (dependencies first)
315
+ *
316
+ * @param measureNames - List of measure names to sort
317
+ * @returns Sorted array of measure names
318
+ * @throws Error if circular dependency detected
319
+ */
320
+ topologicalSort(measureNames: string[]): string[];
321
+ /**
322
+ * Detect circular dependencies using DFS
323
+ * Returns the cycle path if found, null otherwise
324
+ *
325
+ * @returns Array representing the cycle, or null
326
+ */
327
+ detectCycle(): string[] | null;
328
+ /**
329
+ * DFS helper for cycle detection
330
+ */
331
+ private dfs;
332
+ /**
333
+ * Get all dependencies for a specific measure (direct and transitive)
334
+ *
335
+ * @param measureName - Full measure name (e.g., "Cube.measure")
336
+ * @returns Set of all dependency measure names
337
+ */
338
+ getAllDependencies(measureName: string): Set<string>;
339
+ /**
340
+ * Validate that all dependencies exist
341
+ *
342
+ * @param cube - The cube to validate
343
+ * @throws Error if dependencies are missing
344
+ */
345
+ validateDependencies(cube: Cube): void;
346
+ /**
347
+ * Auto-populate dependencies array for calculated measures
348
+ * Updates the measure objects with detected dependencies
349
+ *
350
+ * @param cube - The cube to update
351
+ */
352
+ populateDependencies(cube: Cube): void;
353
+ /**
354
+ * Check if a measure is a calculated measure
355
+ *
356
+ * @param measure - The measure to check
357
+ * @returns True if the measure is calculated
358
+ */
359
+ static isCalculatedMeasure(measure: Measure): boolean;
360
+ }
361
+
362
+ /** Reference to a column for join keys */
363
+ declare interface ColumnRef {
364
+ column: AnyColumn;
365
+ alias?: string;
366
+ }
367
+
368
+ export declare class ComparisonQueryBuilder {
369
+ private dateTimeBuilder;
370
+ constructor(databaseAdapter: DatabaseAdapter);
371
+ /**
372
+ * Check if a query contains compareDateRange
373
+ */
374
+ hasComparison(query: SemanticQuery): boolean;
375
+ /**
376
+ * Get the time dimension with compareDateRange
377
+ */
378
+ getComparisonTimeDimension(query: SemanticQuery): TimeDimension | undefined;
379
+ /**
380
+ * Normalize compareDateRange entries to concrete date ranges
381
+ * Handles both relative strings ('last 30 days') and explicit arrays (['2024-01-01', '2024-01-31'])
382
+ */
383
+ normalizePeriods(compareDateRange: (string | [string, string])[]): NormalizedPeriod[];
384
+ /**
385
+ * Create sub-query for a specific period
386
+ * Replaces compareDateRange with a concrete dateRange for that period
387
+ */
388
+ createPeriodQuery(query: SemanticQuery, period: NormalizedPeriod): SemanticQuery;
389
+ /**
390
+ * Calculate the day-of-period index for a date
391
+ * Used for aligning data points across periods in overlay mode
392
+ */
393
+ calculatePeriodDayIndex(date: Date | string, periodStart: Date, granularity: TimeGranularity): number;
394
+ /**
395
+ * Add period metadata to result rows
396
+ */
397
+ addPeriodMetadata(data: Record<string, unknown>[], period: NormalizedPeriod, timeDimensionKey: string, granularity: TimeGranularity): ComparisonResultRow[];
398
+ /**
399
+ * Merge results from multiple period queries
400
+ * Adds period metadata and creates combined result with annotation
401
+ */
402
+ mergeComparisonResults(periodResults: Array<{
403
+ result: QueryResult;
404
+ period: NormalizedPeriod;
405
+ }>, timeDimension: TimeDimension, granularity: TimeGranularity): QueryResult;
406
+ /**
407
+ * Sort merged results by period index and then by time dimension
408
+ * Ensures consistent ordering for client-side processing
409
+ */
410
+ sortComparisonResults(data: ComparisonResultRow[], timeDimensionKey: string): ComparisonResultRow[];
411
+ }
412
+
413
+ /**
414
+ * Extended result row with period metadata for alignment
415
+ */
416
+ declare interface ComparisonResultRow extends Record<string, unknown> {
417
+ /** Period label (e.g., "2024-01-01 - 2024-01-31") */
418
+ __period: string;
419
+ /** Period index (0 = current, 1 = prior, etc.) */
420
+ __periodIndex: number;
421
+ /** Day-of-period index for alignment in overlay mode */
422
+ __periodDayIndex: number;
423
+ }
424
+
280
425
  /**
281
426
  * Compiled cube with execution function
282
427
  */
@@ -328,6 +473,100 @@ export declare function createPostgresExecutor(db: DrizzleDatabase, schema?: any
328
473
  */
329
474
  export declare function createSQLiteExecutor(db: DrizzleDatabase, schema?: any): SQLiteExecutor;
330
475
 
476
+ /**
477
+ * CTEBuilder handles the construction of Common Table Expressions
478
+ * for pre-aggregation in hasMany relationship queries.
479
+ *
480
+ * This enables efficient aggregation of "many" side data before joining,
481
+ * preventing the Cartesian product explosion that would occur with direct JOINs.
482
+ */
483
+ export declare class CTEBuilder {
484
+ private queryBuilder;
485
+ constructor(queryBuilder: DrizzleSqlBuilder);
486
+ /**
487
+ * Build pre-aggregation CTE for hasMany relationships
488
+ *
489
+ * Creates a CTE that:
490
+ * 1. Selects join keys and aggregated measures
491
+ * 2. Applies security context filtering
492
+ * 3. Groups by join keys and requested dimensions
493
+ * 4. Handles propagating filters from related cubes
494
+ * 5. Handles multi-hop join paths by absorbing intermediate tables (fan-out prevention)
495
+ */
496
+ buildPreAggregationCTE(cteInfo: CTEInfo, query: SemanticQuery, context: QueryContext, queryPlan: PhysicalQueryPlan, preBuiltFilterMap?: Map<string, SQL[]>): any;
497
+ /**
498
+ * Build join condition for CTE
499
+ *
500
+ * Creates the ON clause for joining a CTE to the main query.
501
+ * Uses stored column objects for type-safe joins.
502
+ *
503
+ * For multi-hop paths with intermediate joins:
504
+ * - The CTE includes columns from intermediate tables
505
+ * - The join condition uses the intermediate's primary-connected column
506
+ * - Example: departments.id = employeeteams_agg.department_id (not employee_id!)
507
+ */
508
+ buildCTEJoinCondition(joinCube: PhysicalQueryPlan['joinCubes'][0], cteAlias: string, queryPlan: PhysicalQueryPlan): SQL;
509
+ /**
510
+ * Resolve source-side join expression for CTE joins.
511
+ *
512
+ * When two cubes are both materialized as CTEs in the same query, join keys can
513
+ * still point to the original table column object (e.g. departments.id). In that
514
+ * case the table is no longer present in FROM/JOIN, so rewrite to the upstream CTE
515
+ * alias column (e.g. departments_agg.id).
516
+ */
517
+ private resolveCTEJoinSourceColumn;
518
+ /**
519
+ * Build a subquery filter for propagating filters from related cubes.
520
+ *
521
+ * This generates: cteCube.FK IN (SELECT sourceCube.PK FROM sourceCube WHERE filters...)
522
+ *
523
+ * Example: For Productivity CTE with Employees.createdAt filter:
524
+ * employee_id IN (SELECT id FROM employees WHERE organisation_id = $1 AND created_at >= $date)
525
+ *
526
+ * For composite keys, uses EXISTS instead of IN for better database compatibility:
527
+ * EXISTS (SELECT 1 FROM source WHERE source.pk1 = cte.fk1 AND source.pk2 = cte.fk2 AND <filters>)
528
+ */
529
+ buildPropagatingFilterSubquery(propFilter: PropagatingFilter, context: QueryContext): SQL | null;
530
+ }
531
+
532
+ /**
533
+ * CTE information type extracted from runtime physical plan context
534
+ */
535
+ declare type CTEInfo = NonNullable<PhysicalQueryPlan['preAggregationCTEs']>[0];
536
+
537
+ /**
538
+ * CTE pre-aggregation node.
539
+ * Represents a WITH clause that pre-aggregates a hasMany cube
540
+ * to prevent fan-out in the outer query.
541
+ */
542
+ export declare interface CTEPreAggregate extends LogicalNodeBase {
543
+ readonly type: 'ctePreAggregate';
544
+ /** The cube being pre-aggregated */
545
+ cube: CubeRef;
546
+ /** Table alias for the cube in the main query */
547
+ alias: string;
548
+ /** CTE alias (WITH clause name) */
549
+ cteAlias: string;
550
+ /** Keys connecting CTE back to the main query */
551
+ joinKeys: JoinKeyInfo[];
552
+ /** Measure names included in this CTE */
553
+ measures: string[];
554
+ /** Cross-cube filter propagation */
555
+ propagatingFilters?: PropagatingFilter[];
556
+ /** Downstream join keys for transitive joins through this CTE */
557
+ downstreamJoinKeys?: DownstreamJoinKeyInfo[];
558
+ /** Intermediate joins absorbed into this CTE for fan-out prevention */
559
+ intermediateJoins?: IntermediateJoinInfo[];
560
+ /** CTE type (currently only 'aggregate') */
561
+ cteType: 'aggregate';
562
+ /**
563
+ * Reason for creating this CTE:
564
+ * - 'hasMany': Direct hasMany target — outer query uses SUM
565
+ * - 'fanOutPrevention': Affected by hasMany elsewhere — outer query uses MAX
566
+ */
567
+ cteReason: 'hasMany' | 'fanOutPrevention';
568
+ }
569
+
331
570
  /**
332
571
  * Pre-aggregation CTE analysis
333
572
  */
@@ -339,7 +578,7 @@ export declare type CTEReason = 'hasMany' | 'fanOutPrevention';
339
578
  /**
340
579
  * Cube definition focused on Drizzle query building
341
580
  */
342
- declare interface Cube {
581
+ export declare interface Cube {
343
582
  name: string;
344
583
  title?: string;
345
584
  description?: string;
@@ -375,8 +614,6 @@ declare interface Cube {
375
614
  /** Additional metadata */
376
615
  meta?: Record<string, any>;
377
616
  }
378
- export { Cube }
379
- export { Cube as SemanticCube }
380
617
 
381
618
  /**
382
619
  * Helper type for creating type-safe cubes
@@ -432,7 +669,7 @@ export declare interface CubeDiscoveryResult {
432
669
  /**
433
670
  * Type-safe cube join definition with lazy loading support
434
671
  */
435
- declare interface CubeJoin {
672
+ export declare interface CubeJoin {
436
673
  /** Target cube reference - lazy loaded to avoid circular dependencies */
437
674
  targetCube: Cube | (() => Cube);
438
675
  /** Semantic relationship - determines join behavior */
@@ -479,8 +716,6 @@ declare interface CubeJoin {
479
716
  securitySql?: (securityContext: SecurityContext) => SQL | SQL[];
480
717
  };
481
718
  }
482
- export { CubeJoin }
483
- export { CubeJoin as SemanticJoin }
484
719
 
485
720
  /**
486
721
  * Cube metadata for API responses
@@ -507,6 +742,14 @@ export declare interface CubeMetadata {
507
742
  meta?: Record<string, any>;
508
743
  }
509
744
 
745
+ /** Reference to a registered cube */
746
+ export declare interface CubeRef {
747
+ /** Cube name (e.g. 'Employees') */
748
+ name: string;
749
+ /** Resolved cube object */
750
+ cube: Cube;
751
+ }
752
+
510
753
  /**
511
754
  * Relationship types supported by cube joins
512
755
  */
@@ -749,7 +992,7 @@ export declare function defineCube(name: string, definition: Omit<Cube, 'name'>)
749
992
  /**
750
993
  * Dimension definition
751
994
  */
752
- declare interface Dimension {
995
+ export declare interface Dimension {
753
996
  name: string;
754
997
  title?: string;
755
998
  description?: string;
@@ -778,8 +1021,6 @@ declare interface Dimension {
778
1021
  */
779
1022
  granularities?: TimeGranularity[];
780
1023
  }
781
- export { Dimension }
782
- export { Dimension as SemanticDimension }
783
1024
 
784
1025
  export declare interface DimensionAnnotation {
785
1026
  title: string;
@@ -813,6 +1054,16 @@ export declare interface DimensionMetadata {
813
1054
  granularities?: TimeGranularity[];
814
1055
  }
815
1056
 
1057
+ /** Reference to a dimension on a specific cube */
1058
+ export declare interface DimensionRef {
1059
+ /** Fully qualified name: CubeName.dimensionName */
1060
+ name: string;
1061
+ /** Cube that owns this dimension */
1062
+ cube: CubeRef;
1063
+ /** Local dimension name (without cube prefix) */
1064
+ localName: string;
1065
+ }
1066
+
816
1067
  export declare type DimensionType = 'string' | 'number' | 'time' | 'boolean';
817
1068
 
818
1069
  /**
@@ -880,6 +1131,156 @@ export declare interface DrizzleDatabase {
880
1131
  schema?: unknown;
881
1132
  }
882
1133
 
1134
+ /**
1135
+ * Converts optimised logical plans into runtime physical query plans,
1136
+ * then builds executable Drizzle queries.
1137
+ */
1138
+ export declare class DrizzlePlanBuilder {
1139
+ private readonly queryBuilder;
1140
+ private readonly cteBuilder;
1141
+ private readonly databaseAdapter;
1142
+ constructor(queryBuilder: DrizzleSqlBuilder, cteBuilder: CTEBuilder, databaseAdapter: DatabaseAdapter);
1143
+ /**
1144
+ * Build runtime physical context from an optimised logical plan.
1145
+ * This is the physical-builder input for SQL generation.
1146
+ */
1147
+ derivePhysicalPlanContext(plan: QueryNode): PhysicalQueryPlan;
1148
+ private derivePhysicalPlanContextFromMultiFact;
1149
+ private derivePhysicalPlanContextFromFullKeyAggregate;
1150
+ private toSemanticQuery;
1151
+ private resolvePhysicalSimpleSource;
1152
+ private resolvePhysicalSimpleSourceFromKeysDedup;
1153
+ private resolveKeysDeduplicationMeta;
1154
+ /**
1155
+ * Build unified query that works for both single and multi-cube queries.
1156
+ */
1157
+ build(queryPlan: PhysicalQueryPlan, query: SemanticQuery, context: QueryContext): any;
1158
+ private tryBuildKeysDeduplicationQuery;
1159
+ private tryBuildMultiFactMergeQuery;
1160
+ private buildMultiFactUnionKeysFallbackQuery;
1161
+ private buildSharedKeySelection;
1162
+ private selectRuntimeMergeStrategy;
1163
+ private supportsFullOuterJoin;
1164
+ private coalesceQualifiedColumn;
1165
+ private canExecuteKeysDeduplication;
1166
+ private queryContainsMeasureFilter;
1167
+ private getPrimaryKeyDimensions;
1168
+ /**
1169
+ * Build type-specific outer aggregation for keys deduplication.
1170
+ * Each measure type needs different re-aggregation in the outer query:
1171
+ * - sum/count/number: SUM (re-combine additive values)
1172
+ * - min: MIN (preserve minimum across groups)
1173
+ * - max: MAX (preserve maximum across groups)
1174
+ * - avg: SUM(sums) / NULLIF(SUM(counts), 0) (weighted average from decomposed parts)
1175
+ */
1176
+ private buildKeysOuterAggregation;
1177
+ private applyJoinByType;
1178
+ }
1179
+
1180
+ export declare class DrizzleSqlBuilder {
1181
+ private dateTimeBuilder;
1182
+ private filterBuilder;
1183
+ private groupByBuilder;
1184
+ private measureBuilder;
1185
+ constructor(databaseAdapter: DatabaseAdapter);
1186
+ /**
1187
+ * Build resolvedMeasures map for a set of measures
1188
+ * Delegates to MeasureBuilder
1189
+ */
1190
+ buildResolvedMeasures(measureNames: string[], cubeMap: Map<string, Cube>, context: QueryContext, customMeasureBuilder?: (measureName: string, measure: any, cube: Cube) => SQL): ResolvedMeasures;
1191
+ /**
1192
+ * Build dynamic selections for measures, dimensions, and time dimensions
1193
+ * Works for both single and multi-cube queries
1194
+ * Handles calculated measures with dependency resolution
1195
+ */
1196
+ buildSelections(cubes: Map<string, Cube> | Cube, query: SemanticQuery, context: QueryContext): Record<string, SQL | AnyColumn>;
1197
+ /**
1198
+ * Build calculated measure expression by substituting {member} references
1199
+ * Delegates to MeasureBuilder
1200
+ */
1201
+ buildCalculatedMeasure(measure: any, cube: Cube, allCubes: Map<string, Cube>, resolvedMeasures: ResolvedMeasures, context: QueryContext): SQL;
1202
+ /**
1203
+ * Build resolved measures map for a calculated measure from CTE columns
1204
+ * Delegates to MeasureBuilder
1205
+ */
1206
+ buildCTECalculatedMeasure(measure: any, cube: Cube, cteInfo: {
1207
+ cteAlias: string;
1208
+ measures: string[];
1209
+ cube: Cube;
1210
+ }, allCubes: Map<string, Cube>, context: QueryContext): SQL;
1211
+ /**
1212
+ * Build measure expression for HAVING clause, handling CTE references correctly
1213
+ * Delegates to MeasureBuilder
1214
+ */
1215
+ private buildHavingMeasureExpression;
1216
+ /**
1217
+ * Build measure expression with aggregation and filters
1218
+ * Delegates to MeasureBuilder
1219
+ */
1220
+ buildMeasureExpression(measure: any, context: QueryContext, cube?: Cube): SQL;
1221
+ /**
1222
+ * Build time dimension expression with granularity using database adapter
1223
+ * Delegates to DateTimeBuilder
1224
+ */
1225
+ buildTimeDimensionExpression(dimensionSql: any, granularity: string | undefined, context: QueryContext): SQL;
1226
+ /**
1227
+ * Build WHERE conditions from semantic query filters (dimensions only)
1228
+ * Works for both single and multi-cube queries
1229
+ * @param preBuiltFilters - Optional map of cube name to pre-built filter SQL for parameter deduplication
1230
+ */
1231
+ buildWhereConditions(cubes: Map<string, Cube> | Cube, query: SemanticQuery, context: QueryContext, queryPlan?: PhysicalQueryPlan, preBuiltFilters?: Map<string, SQL[]>): SQL[];
1232
+ /**
1233
+ * Build HAVING conditions from semantic query filters (measures only)
1234
+ * Works for both single and multi-cube queries
1235
+ */
1236
+ buildHavingConditions(cubes: Map<string, Cube> | Cube, query: SemanticQuery, context: QueryContext, queryPlan?: PhysicalQueryPlan): SQL[];
1237
+ /**
1238
+ * Process a single filter (basic or logical)
1239
+ * @param filterType - 'where' for dimension filters, 'having' for measure filters
1240
+ */
1241
+ private processFilter;
1242
+ /**
1243
+ * Build filter condition using Drizzle operators
1244
+ * Delegates to FilterBuilder
1245
+ */
1246
+ private buildFilterCondition;
1247
+ /**
1248
+ * Build date range condition for time dimensions
1249
+ * Delegates to DateTimeBuilder
1250
+ */
1251
+ buildDateRangeCondition(fieldExpr: AnyColumn | SQL, dateRange: string | string[]): SQL | null;
1252
+ /**
1253
+ * Build GROUP BY fields from dimensions and time dimensions
1254
+ * Delegates to GroupByBuilder
1255
+ */
1256
+ buildGroupByFields(cubes: Map<string, Cube> | Cube, query: SemanticQuery, context: QueryContext, queryPlan?: any): (SQL | AnyColumn)[];
1257
+ /**
1258
+ * Build ORDER BY clause with automatic time dimension sorting
1259
+ */
1260
+ buildOrderBy(query: SemanticQuery, selectedFields?: string[]): SQL[];
1261
+ /**
1262
+ * Collect numeric field names (measures + numeric dimensions) for type conversion
1263
+ * Works for both single and multi-cube queries
1264
+ */
1265
+ collectNumericFields(cubes: Map<string, Cube> | Cube, query: SemanticQuery): string[];
1266
+ /**
1267
+ * Apply LIMIT and OFFSET to a query with validation
1268
+ * If offset is provided without limit, add a reasonable default limit
1269
+ */
1270
+ applyLimitAndOffset<T>(query: T, semanticQuery: SemanticQuery): T;
1271
+ /**
1272
+ * Public wrapper for buildFilterCondition - used by executor for cache preloading
1273
+ * This allows pre-building filter SQL before query construction
1274
+ */
1275
+ buildFilterConditionPublic(fieldExpr: AnyColumn | SQL, operator: FilterOperator, values: any[], field?: any, dateRange?: string | string[]): SQL | null;
1276
+ /**
1277
+ * Build a logical filter (AND/OR) - used by executor for cache preloading
1278
+ * This handles nested filter structures and builds combined SQL
1279
+ * Delegates to FilterBuilder
1280
+ */
1281
+ buildLogicalFilter(filter: Filter, cubes: Map<string, Cube>, context: QueryContext): SQL | null;
1282
+ }
1283
+
883
1284
  export declare class DuckDBExecutor extends BaseDatabaseExecutor {
884
1285
  execute<T = any[]>(query: SQL | any, numericFields?: string[]): Promise<T>;
885
1286
  /**
@@ -1137,55 +1538,162 @@ export declare function findBestFieldMatch(metadata: CubeMetadata[], fieldName:
1137
1538
  type: 'measure' | 'dimension';
1138
1539
  } | null;
1139
1540
 
1140
- /**
1141
- * Flow query configuration for server-side execution
1142
- * This is the configuration extracted from SemanticQuery.flow
1143
- */
1144
- declare interface FlowQueryConfig {
1541
+ export declare class FlowQueryBuilder {
1542
+ private filterBuilder;
1543
+ private dateTimeBuilder;
1544
+ private databaseAdapter;
1545
+ constructor(databaseAdapter: DatabaseAdapter);
1145
1546
  /**
1146
- * Binding key that identifies individual entities (e.g., userId)
1147
- * Can be a single string like 'Events.userId' or array for multi-cube
1547
+ * Check if query contains flow configuration
1148
1548
  */
1149
- bindingKey: string | {
1150
- cube: string;
1151
- dimension: string;
1152
- }[];
1549
+ hasFlow(query: SemanticQuery): boolean;
1153
1550
  /**
1154
- * Time dimension used for ordering events
1155
- * Can be a single string like 'Events.timestamp' or array for multi-cube
1551
+ * Validate flow configuration
1156
1552
  */
1157
- timeDimension: string | {
1158
- cube: string;
1159
- dimension: string;
1160
- }[];
1553
+ validateConfig(config: FlowQueryConfig, cubes: Map<string, Cube>): FlowValidationResult;
1161
1554
  /**
1162
- * The starting step from which we explore paths
1163
- * Defines the anchor point for bidirectional flow analysis
1555
+ * Build complete flow query using Drizzle's query builder pattern
1556
+ *
1557
+ * Creates a series of CTEs to:
1558
+ * 1. Find entities at the starting step
1559
+ * 2. Walk backwards N steps to find preceding events
1560
+ * 3. Walk forwards N steps to find following events
1561
+ * 4. Aggregate into nodes and links for Sankey visualization
1164
1562
  */
1165
- startingStep: {
1166
- /** Display name for the starting step */
1167
- name: string;
1168
- /** Filter(s) that identify events for this starting step */
1169
- filter?: Filter | Filter[];
1170
- };
1171
- /** Number of steps to explore BEFORE the starting step (0-5) */
1172
- stepsBefore: number;
1173
- /** Number of steps to explore AFTER the starting step (0-5) */
1174
- stepsAfter: number;
1563
+ buildFlowQuery(config: FlowQueryConfig, cubes: Map<string, Cube>, context: QueryContext): ReturnType<typeof context.db.select>;
1175
1564
  /**
1176
- * Event dimension that categorizes events (e.g., 'Events.eventType')
1177
- * This dimension's values become the node labels in the Sankey diagram
1565
+ * Transform raw SQL result to FlowResultRow
1566
+ * The raw result contains rows with record_type = 'node' or 'link'
1178
1567
  */
1179
- eventDimension: string;
1568
+ transformResult(rawResult: Record<string, unknown>[]): FlowResultRow;
1180
1569
  /**
1181
- * Optional limit on the number of entities to process
1182
- * Useful for performance on large datasets
1570
+ * Resolve flow configuration to SQL expressions
1183
1571
  */
1184
- entityLimit?: number;
1572
+ private resolveFlowConfig;
1185
1573
  /**
1186
- * Output mode for flow data aggregation
1187
- * - 'sankey': Aggregate by (layer, event_type) - standard flow visualization where paths can converge
1188
- * - 'sunburst': Path-qualified nodes for hierarchical tree visualization where each path is unique
1574
+ * Resolve the cube for flow analysis
1575
+ */
1576
+ private resolveCube;
1577
+ /**
1578
+ * Resolve binding key expression
1579
+ */
1580
+ private resolveBindingKey;
1581
+ /**
1582
+ * Resolve time dimension expression
1583
+ */
1584
+ private resolveTimeDimension;
1585
+ /**
1586
+ * Resolve event dimension expression
1587
+ */
1588
+ private resolveEventDimension;
1589
+ /**
1590
+ * Build filter conditions for the starting step
1591
+ */
1592
+ private buildStartingStepFilters;
1593
+ /**
1594
+ * Build a single filter condition
1595
+ */
1596
+ private buildFilterCondition;
1597
+ /**
1598
+ * Build the starting entities CTE
1599
+ * Finds all entities matching the starting step filter with their start time and event type
1600
+ * For sunburst mode, also initializes event_path with the starting event type
1601
+ */
1602
+ private buildStartingEntitiesCTE;
1603
+ /**
1604
+ * Build CTEs for steps BEFORE the starting point using LATERAL joins
1605
+ * Uses ORDER BY ... DESC LIMIT 1 to fetch immediate predecessor via index
1606
+ */
1607
+ private buildBeforeCTEsLateral;
1608
+ /**
1609
+ * Build CTEs for steps AFTER the starting point using LATERAL joins
1610
+ * Uses ORDER BY ... ASC LIMIT 1 to fetch immediate successor via index
1611
+ */
1612
+ private buildAfterCTEsLateral;
1613
+ /**
1614
+ * Build CTEs for steps BEFORE the starting point
1615
+ * Each CTE finds the immediate predecessor event for entities from the previous CTE
1616
+ *
1617
+ * Uses ROW_NUMBER() window function to get exactly the Nth previous event
1618
+ * For sunburst mode, accumulates event_path by prepending to previous path
1619
+ */
1620
+ private buildBeforeCTEsWindow;
1621
+ /**
1622
+ * Build CTEs for steps AFTER the starting point
1623
+ * Each CTE finds the immediate successor event for entities from the previous CTE
1624
+ *
1625
+ * Uses ROW_NUMBER() window function to get exactly the Nth following event
1626
+ * For sunburst mode, accumulates event_path by concatenating with previous path
1627
+ */
1628
+ private buildAfterCTEsWindow;
1629
+ /**
1630
+ * Build the nodes aggregation CTE
1631
+ * Aggregates counts per (layer, event_type) combination using UNION ALL
1632
+ * For sunburst mode, aggregates by event_path for unique tree branches
1633
+ */
1634
+ private buildNodesAggregationCTE;
1635
+ /**
1636
+ * Build the links aggregation CTE
1637
+ * Counts transitions between adjacent layers
1638
+ * For sunburst mode, uses event_path for unique branch identification
1639
+ */
1640
+ private buildLinksAggregationCTE;
1641
+ /**
1642
+ * Build the final result CTE
1643
+ * Combines nodes and links into a single result set with record_type discriminator
1644
+ */
1645
+ private buildFinalResultCTE;
1646
+ }
1647
+
1648
+ /**
1649
+ * Flow query configuration for server-side execution
1650
+ * This is the configuration extracted from SemanticQuery.flow
1651
+ */
1652
+ declare interface FlowQueryConfig {
1653
+ /**
1654
+ * Binding key that identifies individual entities (e.g., userId)
1655
+ * Can be a single string like 'Events.userId' or array for multi-cube
1656
+ */
1657
+ bindingKey: string | {
1658
+ cube: string;
1659
+ dimension: string;
1660
+ }[];
1661
+ /**
1662
+ * Time dimension used for ordering events
1663
+ * Can be a single string like 'Events.timestamp' or array for multi-cube
1664
+ */
1665
+ timeDimension: string | {
1666
+ cube: string;
1667
+ dimension: string;
1668
+ }[];
1669
+ /**
1670
+ * The starting step from which we explore paths
1671
+ * Defines the anchor point for bidirectional flow analysis
1672
+ */
1673
+ startingStep: {
1674
+ /** Display name for the starting step */
1675
+ name: string;
1676
+ /** Filter(s) that identify events for this starting step */
1677
+ filter?: Filter | Filter[];
1678
+ };
1679
+ /** Number of steps to explore BEFORE the starting step (0-5) */
1680
+ stepsBefore: number;
1681
+ /** Number of steps to explore AFTER the starting step (0-5) */
1682
+ stepsAfter: number;
1683
+ /**
1684
+ * Event dimension that categorizes events (e.g., 'Events.eventType')
1685
+ * This dimension's values become the node labels in the Sankey diagram
1686
+ */
1687
+ eventDimension: string;
1688
+ /**
1689
+ * Optional limit on the number of entities to process
1690
+ * Useful for performance on large datasets
1691
+ */
1692
+ entityLimit?: number;
1693
+ /**
1694
+ * Output mode for flow data aggregation
1695
+ * - 'sankey': Aggregate by (layer, event_type) - standard flow visualization where paths can converge
1696
+ * - 'sunburst': Path-qualified nodes for hierarchical tree visualization where each path is unique
1189
1697
  * @default 'sankey'
1190
1698
  */
1191
1699
  outputMode?: 'sankey' | 'sunburst';
@@ -1198,6 +1706,24 @@ declare interface FlowQueryConfig {
1198
1706
  joinStrategy?: 'auto' | 'lateral' | 'window';
1199
1707
  }
1200
1708
 
1709
+ /**
1710
+ * Flow result row returned from query execution
1711
+ * Contains the complete Sankey diagram data
1712
+ */
1713
+ declare interface FlowResultRow {
1714
+ nodes: SankeyNode[];
1715
+ links: SankeyLink[];
1716
+ }
1717
+
1718
+ /**
1719
+ * Flow validation result
1720
+ */
1721
+ declare interface FlowValidationResult {
1722
+ isValid: boolean;
1723
+ errors: string[];
1724
+ warnings: string[];
1725
+ }
1726
+
1201
1727
  /**
1202
1728
  * FNV-1a hash - fast, non-cryptographic hash function
1203
1729
  * Returns hex string for cache key readability
@@ -1235,6 +1761,18 @@ export declare function formatExistingIndexes(indexes: Array<{
1235
1761
  is_primary?: boolean;
1236
1762
  }>): string;
1237
1763
 
1764
+ /**
1765
+ * Full key aggregate: merges results from multiple subqueries
1766
+ * that share the same dimension key set.
1767
+ */
1768
+ declare interface FullKeyAggregate extends LogicalNodeBase {
1769
+ readonly type: 'fullKeyAggregate';
1770
+ /** Subqueries whose results will be merged */
1771
+ subqueries: LogicalNode[];
1772
+ /** Shared dimensions used as the merge key */
1773
+ dimensions: DimensionRef[];
1774
+ }
1775
+
1238
1776
  /**
1239
1777
  * Binding key mapping for multi-cube funnels
1240
1778
  * Maps the user/entity identifier across different cubes
@@ -1254,6 +1792,138 @@ export declare interface FunnelCapabilities {
1254
1792
  supportsIntervalArithmetic: boolean;
1255
1793
  }
1256
1794
 
1795
+ export declare class FunnelQueryBuilder {
1796
+ private databaseAdapter;
1797
+ private filterBuilder;
1798
+ private dateTimeBuilder;
1799
+ constructor(databaseAdapter: DatabaseAdapter);
1800
+ /**
1801
+ * Check if query contains funnel configuration
1802
+ */
1803
+ hasFunnel(query: SemanticQuery): boolean;
1804
+ /**
1805
+ * Validate funnel configuration
1806
+ */
1807
+ validateConfig(config: FunnelQueryConfig, cubes: Map<string, Cube>): {
1808
+ isValid: boolean;
1809
+ errors: string[];
1810
+ };
1811
+ /**
1812
+ * Build complete funnel query using Drizzle's query builder pattern
1813
+ *
1814
+ * Uses the industry-standard "sequential CTEs" pattern where each step
1815
+ * joins to the previous step CTE. This automatically enforces funnel
1816
+ * constraints (monotonically decreasing counts).
1817
+ *
1818
+ * Returns a Drizzle query builder that supports .toSQL() for dry-run
1819
+ * and can be executed directly for results.
1820
+ */
1821
+ buildFunnelQuery(config: FunnelQueryConfig, cubes: Map<string, Cube>, context: QueryContext): ReturnType<typeof context.db.select>;
1822
+ /**
1823
+ * Transform raw SQL result to FunnelResultRow[]
1824
+ */
1825
+ transformResult(rawResult: Record<string, unknown>[], config: FunnelQueryConfig): FunnelResultRow[];
1826
+ /**
1827
+ * Extract cube names referenced in step filters
1828
+ */
1829
+ private extractFilterCubeNames;
1830
+ /**
1831
+ * Resolve steps with their cube, SQL expressions, and filter conditions
1832
+ */
1833
+ private resolveSteps;
1834
+ /**
1835
+ * Resolve the cube for a step
1836
+ */
1837
+ private resolveCubeForStep;
1838
+ /**
1839
+ * Resolve binding key expression for a cube
1840
+ */
1841
+ private resolveBindingKey;
1842
+ /**
1843
+ * Resolve time dimension expression for a cube
1844
+ */
1845
+ private resolveTimeDimension;
1846
+ /**
1847
+ * Build filter conditions for a step
1848
+ * @param step - The funnel step
1849
+ * @param baseCube - The step's primary cube
1850
+ * @param cubes - All cubes available for cross-cube filtering
1851
+ * @param context - Query context with security context
1852
+ */
1853
+ private buildStepFilters;
1854
+ /**
1855
+ * Build a single filter condition
1856
+ * @param filter - The filter to build
1857
+ * @param baseCube - The step's primary cube
1858
+ * @param cubes - All cubes available for cross-cube filtering
1859
+ * @param context - Query context with security context
1860
+ */
1861
+ private buildFilterCondition;
1862
+ /**
1863
+ * Build CTE for a single step using Drizzle's $with() pattern
1864
+ *
1865
+ * For step 0 (entry point): queries raw data directly
1866
+ * For subsequent steps: joins to the previous step CTE to enforce sequential progression
1867
+ *
1868
+ * This implements the industry-standard "sequential CTEs" pattern where each step
1869
+ * only includes binding_keys that successfully completed the previous step.
1870
+ *
1871
+ * @param step - The resolved step configuration
1872
+ * @param context - Query context with security context
1873
+ * @param previousStepCTE - Reference to the previous step's CTE (undefined for step 0)
1874
+ */
1875
+ private buildStepCTE;
1876
+ /**
1877
+ * Build CTE for the first step (step 0) - entry point
1878
+ *
1879
+ * Queries raw data directly with security context and step filters.
1880
+ * Gets the first occurrence per binding key.
1881
+ */
1882
+ private buildFirstStepCTE;
1883
+ /**
1884
+ * Build CTE for subsequent steps (step 1+) - joins to previous step
1885
+ *
1886
+ * This is the key to the sequential funnel pattern:
1887
+ * - INNER JOINs to the previous step CTE (only includes binding_keys that completed previous step)
1888
+ * - Applies temporal constraints (must occur after previous step)
1889
+ * - Applies step-specific filters and time-to-convert windows
1890
+ *
1891
+ * This automatically ensures monotonically decreasing counts.
1892
+ */
1893
+ private buildSubsequentStepCTE;
1894
+ /**
1895
+ * Helper to add cross-cube JOINs to a step query
1896
+ * Extracted to avoid duplication between first and subsequent step methods
1897
+ */
1898
+ private addCrossJoinsToQuery;
1899
+ /**
1900
+ * Build funnel results CTE that joins all step times for time metric calculation
1901
+ *
1902
+ * With the sequential CTE pattern, each step CTE already contains only the
1903
+ * binding_keys that successfully completed that step. This CTE simply joins
1904
+ * them together to enable time difference calculations.
1905
+ *
1906
+ * No CASE expressions needed - the temporal filtering is already done in each step CTE.
1907
+ */
1908
+ private buildFunnelResultsCTE;
1909
+ /**
1910
+ * Build aggregation CTE with counts and optional time metrics
1911
+ *
1912
+ * OPTIMIZATION: Uses single-pass aggregation over funnel_joined CTE instead of
1913
+ * multiple scalar subqueries. This reduces table scans from 13+ to 1 for a typical
1914
+ * 3-step funnel with time metrics.
1915
+ *
1916
+ * - Step counts: COUNT(*) for step_0, COUNT(step_N_time) for subsequent steps
1917
+ * - Time metrics: Uses database-specific conditional aggregation (FILTER clause for
1918
+ * PostgreSQL, CASE WHEN for MySQL/SQLite)
1919
+ * - Percentiles: Still use subqueries since PERCENTILE_CONT with FILTER is non-standard
1920
+ *
1921
+ * Important: All SQL fields must have explicit aliases via .as() for Drizzle
1922
+ * to properly reference them when selecting from the CTE
1923
+ */
1924
+ private buildAggregationCTE;
1925
+ }
1926
+
1257
1927
  /**
1258
1928
  * Funnel query configuration
1259
1929
  */
@@ -1414,6 +2084,15 @@ export declare interface HierarchyMetadata {
1414
2084
  levels: string[];
1415
2085
  }
1416
2086
 
2087
+ /**
2088
+ * No-op optimiser — returns the plan unchanged.
2089
+ * Used as the default in Phase 1 and as a baseline for testing.
2090
+ */
2091
+ export declare class IdentityOptimiser implements PlanOptimiser {
2092
+ readonly name = "identity";
2093
+ optimise(plan: LogicalNode): LogicalNode;
2094
+ }
2095
+
1417
2096
  /**
1418
2097
  * Information about a database index
1419
2098
  */
@@ -1448,6 +2127,19 @@ export declare interface IntermediateJoinInfo {
1448
2127
  cteJoinColumn: AnyColumn;
1449
2128
  }
1450
2129
 
2130
+ /**
2131
+ * Internal representation of a join path step
2132
+ * Used during path finding - simpler than the public JoinPathStep analysis type
2133
+ */
2134
+ declare interface InternalJoinPathStep {
2135
+ /** Source cube name */
2136
+ fromCube: string;
2137
+ /** Target cube name */
2138
+ toCube: string;
2139
+ /** The join definition from the source cube */
2140
+ joinDef: CubeJoin;
2141
+ }
2142
+
1451
2143
  /**
1452
2144
  * Type guard for multi-cube binding key
1453
2145
  */
@@ -1480,6 +2172,29 @@ export declare function isRetentionQuery(query: unknown): query is {
1480
2172
  retention: RetentionQueryConfig;
1481
2173
  };
1482
2174
 
2175
+ /**
2176
+ * Planned join entry used by runtime physical builders.
2177
+ */
2178
+ export declare interface JoinCubePlanEntry {
2179
+ cube: Cube;
2180
+ alias: string;
2181
+ joinType: 'inner' | 'left' | 'right' | 'full';
2182
+ joinCondition: SQL;
2183
+ /** Relationship type from the join definition that produced this entry */
2184
+ relationship?: 'belongsTo' | 'hasOne' | 'hasMany' | 'belongsToMany';
2185
+ /** Junction table information for belongsToMany relationships */
2186
+ junctionTable?: {
2187
+ table: Table;
2188
+ alias: string;
2189
+ joinType: 'inner' | 'left' | 'right' | 'full';
2190
+ joinCondition: SQL;
2191
+ /** Optional security SQL function to apply to junction table */
2192
+ securitySql?: (securityContext: SecurityContext) => SQL | SQL[];
2193
+ /** Source cube name for the belongsToMany relationship (needed for CTE rewriting) */
2194
+ sourceCubeName?: string;
2195
+ };
2196
+ }
2197
+
1483
2198
  /**
1484
2199
  * Join key information for CTE joins
1485
2200
  * Describes how a CTE should be joined to the main query
@@ -1521,6 +2236,114 @@ export declare interface JoinPathAnalysis {
1521
2236
  error?: string;
1522
2237
  /** Cubes that were visited during BFS search */
1523
2238
  visitedCubes?: string[];
2239
+ /** Path selection decision and scoring details (when preferred routing is used) */
2240
+ selection?: {
2241
+ strategy: 'shortest' | 'preferred' | 'fallbackShortest';
2242
+ preferredCubes?: string[];
2243
+ selectedRank?: number;
2244
+ selectedScore?: number;
2245
+ candidates?: Array<{
2246
+ rank: number;
2247
+ score: number;
2248
+ usesPreferredJoin: boolean;
2249
+ preferredCubesInPath: number;
2250
+ usesProcessed: boolean;
2251
+ scoreBreakdown: {
2252
+ preferredJoinBonus: number;
2253
+ preferredCubeBonus: number;
2254
+ lengthPenalty: number;
2255
+ };
2256
+ path: JoinPathStep[];
2257
+ }>;
2258
+ };
2259
+ }
2260
+
2261
+ /**
2262
+ * Resolves join paths between cubes and manages connectivity caching
2263
+ */
2264
+ export declare class JoinPathResolver {
2265
+ private cubes;
2266
+ private connectivityCache;
2267
+ /**
2268
+ * @param cubes Map of cube name to cube definition
2269
+ */
2270
+ constructor(cubes: Map<string, Cube>);
2271
+ /**
2272
+ * Find the shortest join path from source cube to target cube
2273
+ * Uses BFS algorithm for optimal path discovery
2274
+ *
2275
+ * @param fromCube Source cube name
2276
+ * @param toCube Target cube name
2277
+ * @param alreadyProcessed Set of cubes to exclude from path finding
2278
+ * @returns Array of join steps or null if no path exists
2279
+ */
2280
+ findPath(fromCube: string, toCube: string, alreadyProcessed?: Set<string>): InternalJoinPathStep[] | null;
2281
+ /**
2282
+ * Find path that prefers going through specified cubes when possible
2283
+ * Used when certain cubes have measures in the query - ensures joins go through
2284
+ * the semantically correct path (e.g., through junction tables when their measures are used)
2285
+ *
2286
+ * IMPORTANT: This method allows paths to go THROUGH already-processed cubes (as intermediate
2287
+ * steps) but won't return them as new cubes to add. This is crucial for preferring paths
2288
+ * through cubes that have measures (like junction tables).
2289
+ *
2290
+ * Path scoring priority (highest to lowest):
2291
+ * 1. Paths using joins with `preferredFor` that includes the target cube (score +10)
2292
+ * 2. Paths going through cubes with measures in the query (score +1 per cube)
2293
+ * 3. Paths reusing already-processed cubes
2294
+ * 4. Shorter paths
2295
+ *
2296
+ * @param fromCube Source cube name
2297
+ * @param toCube Target cube name
2298
+ * @param preferredCubes Set of cube names to prefer in the path (usually cubes with measures)
2299
+ * @param alreadyProcessed Set of cubes already in the join plan (can be used as intermediates)
2300
+ * @returns Array of join steps or null if no path exists
2301
+ */
2302
+ findPathPreferring(fromCube: string, toCube: string, preferredCubes: Set<string>, alreadyProcessed?: Set<string>): InternalJoinPathStep[] | null;
2303
+ /**
2304
+ * Find preferred path with candidate scoring telemetry.
2305
+ * Used by analysis/debug panels to explain planner decisions.
2306
+ */
2307
+ findPathPreferringDetailed(fromCube: string, toCube: string, preferredCubes: Set<string>, alreadyProcessed?: Set<string>): PreferredPathSelection;
2308
+ /**
2309
+ * Find all possible paths between two cubes (up to maxDepth)
2310
+ * Used by findPathPreferring to evaluate multiple paths
2311
+ *
2312
+ * @param fromCube Source cube name
2313
+ * @param toCube Target cube name
2314
+ * @param alreadyProcessed Set of cubes to exclude from path finding
2315
+ * @param maxDepth Maximum path length to search (default 4 to avoid explosion)
2316
+ * @returns Array of all valid paths
2317
+ */
2318
+ private findAllPaths;
2319
+ /**
2320
+ * Check if a cube can reach all other cubes in the list via joins
2321
+ *
2322
+ * @param fromCube Starting cube name
2323
+ * @param allCubes List of all cubes that must be reachable
2324
+ * @returns true if all cubes are reachable
2325
+ */
2326
+ canReachAll(fromCube: string, allCubes: string[]): boolean;
2327
+ /**
2328
+ * Build SQL join condition from join definition
2329
+ *
2330
+ * @param joinDef The cube join definition
2331
+ * @param sourceAlias Optional alias for source table (null uses actual column)
2332
+ * @param targetAlias Optional alias for target table (null uses actual column)
2333
+ * @returns SQL condition for the join
2334
+ */
2335
+ buildJoinCondition(joinDef: CubeJoin, sourceAlias: string | null, targetAlias: string | null): SQL;
2336
+ /**
2337
+ * Get all reachable cubes from a starting cube
2338
+ * Useful for analyzing cube connectivity
2339
+ *
2340
+ * @param fromCube Starting cube name
2341
+ * @returns Set of all reachable cube names
2342
+ */
2343
+ getReachableCubes(fromCube: string): Set<string>;
2344
+ private getCacheKey;
2345
+ private getFromCache;
2346
+ private setInCache;
1524
2347
  }
1525
2348
 
1526
2349
  /**
@@ -1548,41 +2371,382 @@ export declare interface JoinPathStep {
1548
2371
  };
1549
2372
  }
1550
2373
 
2374
+ declare interface JoinRef {
2375
+ /** Target cube being joined */
2376
+ target: CubeRef;
2377
+ /** Table alias in the generated SQL */
2378
+ alias: string;
2379
+ /** SQL join type */
2380
+ joinType: 'inner' | 'left' | 'right' | 'full';
2381
+ /** Drizzle SQL join condition (pre-built by the planner) */
2382
+ joinCondition: SQL;
2383
+ /** Relationship type from the join definition */
2384
+ relationship?: 'belongsTo' | 'hasOne' | 'hasMany' | 'belongsToMany';
2385
+ /** Junction table for belongsToMany relationships */
2386
+ junctionTable?: {
2387
+ table: Table;
2388
+ alias: string;
2389
+ joinType: 'inner' | 'left' | 'right' | 'full';
2390
+ joinCondition: SQL;
2391
+ securitySql?: (securityContext: SecurityContext) => SQL | SQL[];
2392
+ sourceCubeName?: string;
2393
+ };
2394
+ }
2395
+
1551
2396
  export declare type JoinType = 'left' | 'right' | 'inner' | 'full';
1552
2397
 
2398
+ /**
2399
+ * Keys-based deduplication for multiplied measures.
2400
+ */
2401
+ declare interface KeysDeduplication extends LogicalNodeBase {
2402
+ readonly type: 'keysDeduplication';
2403
+ /** SELECT DISTINCT PKs + dims */
2404
+ keysSource: LogicalNode;
2405
+ /** Aggregated measures source */
2406
+ measureSource: LogicalNode;
2407
+ /** Primary key columns to join on */
2408
+ joinOn: ColumnRef[];
2409
+ /** Measure names NOT from the multiplied cube (pre-aggregated in keys CTE) */
2410
+ regularMeasures?: string[];
2411
+ }
2412
+
1553
2413
  export declare interface LogicalFilter {
1554
2414
  and?: Filter[];
1555
2415
  or?: Filter[];
1556
2416
  }
1557
2417
 
2418
+ export declare type LogicalNode = QueryNode | SimpleSource | FullKeyAggregate | CTEPreAggregate | KeysDeduplication | MultiFactMerge;
2419
+
2420
+ /** Base interface shared by all logical plan nodes */
2421
+ declare interface LogicalNodeBase {
2422
+ readonly type: LogicalNodeType;
2423
+ readonly schema: LogicalSchema;
2424
+ }
2425
+
2426
+ /** Discriminated union tag for all node types */
2427
+ declare type LogicalNodeType = 'query' | 'simpleSource' | 'fullKeyAggregate' | 'ctePreAggregate' | 'keysDeduplication' | 'multiFactMerge';
2428
+
2429
+ export declare class LogicalPlanBuilder {
2430
+ private readonly queryPlanner;
2431
+ constructor(queryPlanner: LogicalPlanner);
2432
+ /**
2433
+ * Build a logical plan from a semantic query.
2434
+ */
2435
+ plan(cubes: Map<string, Cube>, query: SemanticQuery, ctx: QueryContext): QueryNode;
2436
+ /**
2437
+ * Build a logical plan and an explicit decision trace for dry-run/explain.
2438
+ * The analysis is produced from the same phase outputs used to build the plan.
2439
+ */
2440
+ planWithAnalysis(cubes: Map<string, Cube>, query: SemanticQuery, ctx: QueryContext): LogicalPlanWithAnalysis;
2441
+ private buildSourceFromPhases;
2442
+ private buildSimpleSourceFromPhases;
2443
+ /**
2444
+ * Detect and build multi-fact merge for star-schema style queries where:
2445
+ * - measures come from 2+ cubes
2446
+ * - all grouping dimensions/timeDimensions come from one shared dimension cube
2447
+ * - each measure cube directly belongsTo/hasOne that shared cube
2448
+ */
2449
+ private tryBuildMultiFactMergeSource;
2450
+ private hasDirectJoinToSharedDimension;
2451
+ private buildGroupQueryNode;
2452
+ private projectFiltersToAllowedCubes;
2453
+ private projectFilterNodeToAllowedCubes;
2454
+ private classifyMeasuresForStrategy;
2455
+ private selectMeasureStrategy;
2456
+ /**
2457
+ * Initial execution scope for keys deduplication:
2458
+ * - exactly one multiplied cube
2459
+ * - all query measures belong to that cube
2460
+ * - only additive measure types currently handled by the physical keys path
2461
+ * - no measure filters (HAVING) in the query
2462
+ */
2463
+ private isKeysDeduplicationExecutionSupported;
2464
+ private queryHasMeasureFilter;
2465
+ private buildKeysDeduplicationSource;
2466
+ private isDeduplicationSafeMeasure;
2467
+ private getPrimaryKeyColumns;
2468
+ private deduplicateColumnRefs;
2469
+ private buildQueryNode;
2470
+ private buildPreAggregationAnalysis;
2471
+ private buildMeasureRefs;
2472
+ private buildDimensionRefs;
2473
+ private buildTimeDimensionRefs;
2474
+ private buildOrderByRefs;
2475
+ private toCubeRef;
2476
+ private buildCTESchema;
2477
+ }
2478
+
1558
2479
  /**
1559
- * Measure definition
2480
+ * Pre-aggregation plan for handling hasMany relationships
1560
2481
  */
1561
- declare interface Measure {
1562
- name: string;
1563
- title?: string;
1564
- description?: string;
2482
+ export declare class LogicalPlanner {
2483
+ private resolverCache;
1565
2484
  /**
1566
- * Alternative names for this measure
1567
- * Used by AI agents for natural language matching
1568
- * @example ['revenue', 'sales', 'income'] for a totalRevenue measure
2485
+ * Get or create a JoinPathResolver for the given cubes map
1569
2486
  */
1570
- synonyms?: string[];
1571
- type: MeasureType;
2487
+ private getResolver;
1572
2488
  /**
1573
- * Column to aggregate or SQL expression
1574
- * Optional for calculated measures (type: 'calculated') which use calculatedSql instead
2489
+ * Analyze a semantic query to determine which cubes are involved
1575
2490
  */
1576
- sql?: AnyColumn | SQL | ((ctx: QueryContext) => AnyColumn | SQL);
1577
- /** Display format */
1578
- format?: string;
1579
- /** Whether to show in UI */
1580
- shown?: boolean;
1581
- /** Filters applied to this measure */
1582
- filters?: Array<(ctx: QueryContext) => SQL>;
1583
- /** Rolling window configuration */
1584
- rollingWindow?: {
1585
- trailing?: string;
2491
+ analyzeCubeUsage(query: SemanticQuery): Set<string>;
2492
+ /**
2493
+ * Build query-level path hints (Cube-style) from all query members:
2494
+ * measures, dimensions, time dimensions, filters, and order-by.
2495
+ * These hints guide path selection toward the semantic query grain.
2496
+ */
2497
+ private collectPathHintCubes;
2498
+ /**
2499
+ * Recursively extract cube names from filters (handles logical filters)
2500
+ */
2501
+ private extractCubeNamesFromFilter;
2502
+ /**
2503
+ * Extract measures referenced in filters (for CTE inclusion)
2504
+ */
2505
+ private extractMeasuresFromFilters;
2506
+ /**
2507
+ * Recursively extract measures from filters for a specific cube
2508
+ * Only includes filter members that are actually measures (not dimensions)
2509
+ */
2510
+ private extractMeasuresFromFilter;
2511
+ /**
2512
+ * Choose the primary cube based on query analysis
2513
+ * Uses a consistent strategy to avoid measure order dependencies
2514
+ *
2515
+ * Delegates to analyzePrimaryCubeSelection() for the actual logic,
2516
+ * ensuring a single source of truth for primary cube selection.
2517
+ */
2518
+ choosePrimaryCube(cubeNames: string[], query: SemanticQuery, cubes?: Map<string, Cube>): string;
2519
+ /**
2520
+ * Analyze primary cube selection with candidate details.
2521
+ * Exposed for LogicalPlanBuilder so dry-run/analyze can report
2522
+ * exactly which selection rule was used.
2523
+ */
2524
+ analyzePrimaryCube(cubeNames: string[], query: SemanticQuery, cubes: Map<string, Cube>): PrimaryCubeAnalysis;
2525
+ /**
2526
+ * Analyze join path for a specific target cube.
2527
+ * Exposed for LogicalPlanBuilder to provide join decision trace.
2528
+ */
2529
+ analyzeJoinPathForTarget(cubes: Map<string, Cube>, fromCube: string, toCube: string, query?: SemanticQuery): JoinPathAnalysis;
2530
+ /**
2531
+ * Build join plan for a known primary cube.
2532
+ * Exposed for LogicalPlanBuilder so logical planning can compose
2533
+ * planner phases directly.
2534
+ */
2535
+ buildJoinPlanForPrimary(cubes: Map<string, Cube>, primaryCube: Cube, cubeNames: string[], ctx: QueryContext, query: SemanticQuery): PhysicalQueryPlan['joinCubes'];
2536
+ /**
2537
+ * Build pre-aggregation CTE plan from a primary cube and join plan.
2538
+ * Exposed for LogicalPlanBuilder phase composition.
2539
+ */
2540
+ buildPreAggregationCTEs(cubes: Map<string, Cube>, primaryCube: Cube, joinCubes: PhysicalQueryPlan['joinCubes'], query: SemanticQuery, ctx: QueryContext): PhysicalQueryPlan['preAggregationCTEs'];
2541
+ /**
2542
+ * Generate query warnings from pre-aggregation analysis.
2543
+ * Exposed for LogicalPlanBuilder phase composition.
2544
+ */
2545
+ buildWarnings(query: SemanticQuery, preAggregationCTEs?: PhysicalQueryPlan['preAggregationCTEs']): QueryWarning[];
2546
+ /**
2547
+ * Build join plan for multi-cube query
2548
+ * Supports both direct joins and transitive joins through intermediate cubes
2549
+ *
2550
+ * Uses query-aware path selection to prefer joining through cubes that have
2551
+ * measures in the query (e.g., joining Teams through EmployeeTeams when
2552
+ * EmployeeTeams.count is a measure)
2553
+ */
2554
+ private buildJoinPlan;
2555
+ /**
2556
+ * Plan pre-aggregation CTEs for hasMany relationships to prevent fan-out
2557
+ * Note: belongsToMany relationships handle fan-out differently through their junction table structure
2558
+ * and don't require CTEs - the two-hop join with the junction table provides natural grouping
2559
+ *
2560
+ * CRITICAL FAN-OUT PREVENTION LOGIC:
2561
+ * When a query contains ANY hasMany relationship in the join graph, ALL cubes with measures
2562
+ * that could be affected by row multiplication need CTEs. This includes:
2563
+ *
2564
+ * 1. Cubes with direct hasMany FROM primary (existing logic)
2565
+ * 2. Cubes with measures that would be multiplied due to hasMany elsewhere in the query
2566
+ * - Example: Query has Departments.totalBudget + Productivity.recordCount
2567
+ * - Employees hasMany → Productivity causes row multiplication
2568
+ * - Departments.totalBudget would be inflated without CTE pre-aggregation
2569
+ */
2570
+ private planPreAggregationCTEs;
2571
+ /**
2572
+ * Find join information TO a cube (reverse lookup)
2573
+ * Used when the primary cube needs a CTE and we need to find how other cubes join to it
2574
+ */
2575
+ private findJoinInfoToCube;
2576
+ /**
2577
+ * Analyze the join path from primary cube to a target CTE cube.
2578
+ * Detects if there are intermediate hasMany relationships that would cause fan-out.
2579
+ *
2580
+ * Returns information about:
2581
+ * - The full join path
2582
+ * - Whether there are hasMany relationships ON the path (not just at the end)
2583
+ * - Which intermediate tables need to be absorbed into the CTE
2584
+ * - The correct join key to use (from primary cube's connection point)
2585
+ *
2586
+ * @param cubes Map of all registered cubes
2587
+ * @param primaryCube The primary cube (FROM clause)
2588
+ * @param targetCubeName The CTE cube we're analyzing the path to
2589
+ * @param ctx Query context for security filtering
2590
+ */
2591
+ private analyzeJoinPathToPrimary;
2592
+ /**
2593
+ * Compute CTE reasons from the actual join plan entries.
2594
+ *
2595
+ * Instead of scanning all registered cubes (which causes false positives when
2596
+ * unrelated hasMany relationships exist), this walks only the planned joins
2597
+ * using the `relationship` field now stored on each JoinCubePlanEntry.
2598
+ *
2599
+ * Algorithm:
2600
+ * 1. Scan join plan entries for hasMany/belongsToMany relationships
2601
+ * 2. If none found → return empty map (no CTEs needed)
2602
+ * 3. hasMany/belongsToMany targets with measures → 'hasMany'
2603
+ * 4. Other join cubes with measures (not hasMany source) → 'fanOutPrevention'
2604
+ */
2605
+ private computeCTEReasons;
2606
+ /**
2607
+ * Find join information for a cube from any cube in the query
2608
+ * This extends findHasManyJoinDef to work with any relationship type
2609
+ * and to search from any source cube, not just the primary
2610
+ */
2611
+ private findJoinInfoForCube;
2612
+ /**
2613
+ * Find downstream cubes that need join keys included in the CTE.
2614
+ *
2615
+ * When a query has dimensions from a cube (e.g., Teams.name) and measures from
2616
+ * a junction cube (e.g., EmployeeTeams.count), the junction CTE needs to include
2617
+ * the join key to the dimension cube (team_id) so the dimension cube can be
2618
+ * joined through the CTE instead of via an alternative path.
2619
+ *
2620
+ * @param cteCube The cube being converted to a CTE (e.g., EmployeeTeams)
2621
+ * @param query The semantic query with dimensions and measures
2622
+ * @param allCubes Map of all registered cubes
2623
+ * @returns Array of downstream join key info for cubes needing join through this CTE
2624
+ */
2625
+ private findDownstreamJoinKeys;
2626
+ /**
2627
+ * Expand calculated measures to include their dependencies
2628
+ */
2629
+ private expandCalculatedMeasureDependencies;
2630
+ /**
2631
+ * Extract measure references from calculatedSql template
2632
+ */
2633
+ private extractDependenciesFromTemplate;
2634
+ /**
2635
+ * Find hasMany join definition from primary cube to target cube
2636
+ */
2637
+ private findHasManyJoinDef;
2638
+ /**
2639
+ * Find filters that need to propagate from related cubes to a CTE cube.
2640
+ * When cube A has filters and a hasMany relationship to cube B (the CTE cube),
2641
+ * A's filters should propagate into B's CTE via a subquery.
2642
+ *
2643
+ * Example: Employees.createdAt filter should propagate to Productivity CTE
2644
+ * via: employee_id IN (SELECT id FROM employees WHERE created_at >= $date)
2645
+ */
2646
+ private findPropagatingFilters;
2647
+ /**
2648
+ * Extract cube names from filters into a Set (helper for findPropagatingFilters)
2649
+ */
2650
+ private extractFilterCubeNamesToSet;
2651
+ /**
2652
+ * Extract filters for a specific cube from the filter array
2653
+ *
2654
+ * Logic for preserving filter semantics:
2655
+ * - AND: Safe to extract only matching branches (AND of fewer conditions is more permissive)
2656
+ * - OR: Must include ALL branches or skip entirely (partial OR changes semantics)
2657
+ * If any branch belongs to another cube, skip the entire OR to be safe
2658
+ * since we can't evaluate the other cube's conditions
2659
+ */
2660
+ private extractFiltersForCube;
2661
+ /**
2662
+ * Check if all simple filters in a filter array belong to the specified cube
2663
+ * Recursively checks nested AND/OR filters
2664
+ */
2665
+ private allFiltersFromCube;
2666
+ /**
2667
+ * Extract time dimension date range filters as regular filters for a specific cube
2668
+ */
2669
+ private extractTimeDimensionFiltersForCube;
2670
+ /**
2671
+ * Analyze why a particular cube was chosen as primary
2672
+ */
2673
+ private analyzePrimaryCubeSelection;
2674
+ /**
2675
+ * Analyze the join path between two cubes with detailed step information
2676
+ *
2677
+ * Uses JoinPathResolver.findPath() for the actual path finding,
2678
+ * then converts the result to human-readable analysis format.
2679
+ */
2680
+ private analyzeJoinPath;
2681
+ private convertInternalPathToJoinPathSteps;
2682
+ private buildJoinPathSelectionAnalysis;
2683
+ private mapPreferredCandidate;
2684
+ /**
2685
+ * Generate warnings for query edge cases that users should be aware of.
2686
+ * Currently detects:
2687
+ * - FAN_OUT_NO_DIMENSIONS: Query has hasMany CTEs but no dimensions to group by
2688
+ *
2689
+ * Note: AVG measures in hasMany CTEs can produce mathematically imprecise results
2690
+ * (average of averages vs weighted average), but this warning was removed as it
2691
+ * fired too aggressively. The issue only occurs when the outer grouping is coarser
2692
+ * than the CTE grouping, which is rare in practice. The limitation is documented
2693
+ * in executor.ts comments.
2694
+ */
2695
+ private generateWarnings;
2696
+ /**
2697
+ * Detect when a query has measures from multiple cubes with hasMany relationships
2698
+ * but no dimensions to provide grouping context.
2699
+ *
2700
+ * This is an edge case where:
2701
+ * - Query has measures from 2+ cubes
2702
+ * - At least one CTE exists (indicating hasMany relationship)
2703
+ * - Query has NO dimensions AND NO time dimensions with granularity
2704
+ *
2705
+ * The SQL is technically correct (CTEs with GROUP BY on join keys), but users
2706
+ * may be confused by the aggregated results without visible grouping.
2707
+ */
2708
+ private checkFanOutNoDimensions;
2709
+ }
2710
+
2711
+ export declare interface LogicalPlanWithAnalysis {
2712
+ plan: QueryNode;
2713
+ analysis: QueryAnalysis;
2714
+ }
2715
+
2716
+ declare interface LogicalSchema {
2717
+ measures: MeasureRef[];
2718
+ dimensions: DimensionRef[];
2719
+ timeDimensions: TimeDimensionRef[];
2720
+ }
2721
+
2722
+ /**
2723
+ * Measure definition
2724
+ */
2725
+ export declare interface Measure {
2726
+ name: string;
2727
+ title?: string;
2728
+ description?: string;
2729
+ /**
2730
+ * Alternative names for this measure
2731
+ * Used by AI agents for natural language matching
2732
+ * @example ['revenue', 'sales', 'income'] for a totalRevenue measure
2733
+ */
2734
+ synonyms?: string[];
2735
+ type: MeasureType;
2736
+ /**
2737
+ * Column to aggregate or SQL expression
2738
+ * Optional for calculated measures (type: 'calculated') which use calculatedSql instead
2739
+ */
2740
+ sql?: AnyColumn | SQL | ((ctx: QueryContext) => AnyColumn | SQL);
2741
+ /** Display format */
2742
+ format?: string;
2743
+ /** Whether to show in UI */
2744
+ shown?: boolean;
2745
+ /** Filters applied to this measure */
2746
+ filters?: Array<(ctx: QueryContext) => SQL>;
2747
+ /** Rolling window configuration */
2748
+ rollingWindow?: {
2749
+ trailing?: string;
1586
2750
  leading?: string;
1587
2751
  offset?: string;
1588
2752
  };
@@ -1677,8 +2841,6 @@ declare interface Measure {
1677
2841
  };
1678
2842
  };
1679
2843
  }
1680
- export { Measure }
1681
- export { Measure as SemanticMeasure }
1682
2844
 
1683
2845
  /**
1684
2846
  * Annotation interfaces for UI metadata
@@ -1690,6 +2852,18 @@ export declare interface MeasureAnnotation {
1690
2852
  format?: MeasureFormat;
1691
2853
  }
1692
2854
 
2855
+ /**
2856
+ * Dependency information for a calculated measure
2857
+ */
2858
+ declare interface MeasureDependency {
2859
+ /** Full measure name (e.g., "Cube.measure" or "measure") */
2860
+ measureName: string;
2861
+ /** Referenced cube name (null if same cube) */
2862
+ cubeName: string | null;
2863
+ /** Referenced field name */
2864
+ fieldName: string;
2865
+ }
2866
+
1693
2867
  export declare type MeasureFormat = 'currency' | 'percent' | 'number' | 'integer';
1694
2868
 
1695
2869
  /**
@@ -1715,6 +2889,16 @@ export declare interface MeasureMetadata {
1715
2889
  drillMembers?: string[];
1716
2890
  }
1717
2891
 
2892
+ /** Reference to a measure on a specific cube */
2893
+ export declare interface MeasureRef {
2894
+ /** Fully qualified name: CubeName.measureName */
2895
+ name: string;
2896
+ /** Cube that owns this measure */
2897
+ cube: CubeRef;
2898
+ /** Local measure name (without cube prefix) */
2899
+ localName: string;
2900
+ }
2901
+
1718
2902
  /**
1719
2903
  * Type enums and constants
1720
2904
  */
@@ -1836,6 +3020,20 @@ export declare interface MultiCubeQueryContext extends QueryContext {
1836
3020
  currentCube: Cube;
1837
3021
  }
1838
3022
 
3023
+ /**
3024
+ * Multi-fact merge: combines independent fact subqueries
3025
+ * that share dimensions.
3026
+ */
3027
+ declare interface MultiFactMerge extends LogicalNodeBase {
3028
+ readonly type: 'multiFactMerge';
3029
+ /** Independent fact subqueries */
3030
+ groups: LogicalNode[];
3031
+ /** Shared dimensions for the merge */
3032
+ sharedDimensions: DimensionRef[];
3033
+ /** How to combine the groups */
3034
+ mergeStrategy: 'fullJoin' | 'leftJoin' | 'innerJoin';
3035
+ }
3036
+
1839
3037
  export declare type MySQLDatabase = MySql2Database<any>;
1840
3038
 
1841
3039
  export declare class MySQLExecutor extends BaseDatabaseExecutor {
@@ -1859,6 +3057,20 @@ export declare class MySQLExecutor extends BaseDatabaseExecutor {
1859
3057
  getTableIndexes(tableNames: string[]): Promise<IndexInfo[]>;
1860
3058
  }
1861
3059
 
3060
+ /**
3061
+ * Normalized period range with start/end dates and metadata
3062
+ */
3063
+ declare interface NormalizedPeriod {
3064
+ /** Start date of the period */
3065
+ start: Date;
3066
+ /** End date of the period */
3067
+ end: Date;
3068
+ /** Human-readable label for the period */
3069
+ label: string;
3070
+ /** Index in the comparison (0 = first/current, 1 = second/prior, etc.) */
3071
+ index: number;
3072
+ }
3073
+
1862
3074
  /**
1863
3075
  * Normalize query for consistent hashing
1864
3076
  * Sorts arrays and object keys to ensure same query = same hash
@@ -1868,6 +3080,31 @@ export declare class MySQLExecutor extends BaseDatabaseExecutor {
1868
3080
  */
1869
3081
  export declare function normalizeQuery(query: SemanticQuery): SemanticQuery;
1870
3082
 
3083
+ /** Context available to optimiser passes */
3084
+ export declare interface OptimiserContext {
3085
+ /** Database engine type for engine-specific rewrites */
3086
+ engineType: 'postgres' | 'mysql' | 'sqlite' | 'duckdb';
3087
+ }
3088
+
3089
+ /**
3090
+ * Runs a sequence of optimiser passes in order.
3091
+ * Short-circuits if a pass returns a different reference (for future use).
3092
+ */
3093
+ export declare class OptimiserPipeline implements PlanOptimiser {
3094
+ readonly name = "pipeline";
3095
+ private passes;
3096
+ constructor(passes?: PlanOptimiser[]);
3097
+ optimise(plan: LogicalNode, context: OptimiserContext): LogicalNode;
3098
+ addPass(pass: PlanOptimiser): void;
3099
+ }
3100
+
3101
+ /** Order-by specification */
3102
+ declare interface OrderByRef {
3103
+ /** Fully qualified field name */
3104
+ name: string;
3105
+ direction: 'asc' | 'desc';
3106
+ }
3107
+
1871
3108
  /**
1872
3109
  * Period comparison metadata for compareDateRange queries
1873
3110
  * Provides information about the periods being compared
@@ -1884,20 +3121,86 @@ export declare interface PeriodComparisonMetadata {
1884
3121
  }
1885
3122
 
1886
3123
  /**
1887
- * Type helpers for specific database types
3124
+ * Runtime physical-plan context used by query execution builders.
3125
+ * This is the only runtime plan context consumed by SQL builders.
1888
3126
  */
1889
- export declare type PostgresDatabase = PostgresJsDatabase<any>;
1890
-
1891
- export declare class PostgresExecutor extends BaseDatabaseExecutor {
1892
- execute<T = any[]>(query: SQL | any, numericFields?: string[]): Promise<T>;
3127
+ export declare interface PhysicalQueryPlan {
3128
+ /** Primary cube that drives the query */
3129
+ primaryCube: Cube;
3130
+ /** Additional cubes to join (empty for single-cube queries) */
3131
+ joinCubes: JoinCubePlanEntry[];
3132
+ /** Pre-aggregation CTEs for hasMany relationships to prevent fan-out */
3133
+ preAggregationCTEs?: PreAggregationCTEInfo[];
1893
3134
  /**
1894
- * Convert numeric string fields to numbers (only for measure fields)
3135
+ * Optional keys-based deduplication metadata.
3136
+ * When present, physical builders may choose a keys+aggregate execution path.
1895
3137
  */
1896
- private convertNumericFields;
3138
+ keysDeduplication?: {
3139
+ multipliedCubeName: string;
3140
+ primaryKeyDimensions: string[];
3141
+ /** Measure names NOT from the multiplied cube (pre-aggregated in keys CTE) */
3142
+ regularMeasures?: string[];
3143
+ };
1897
3144
  /**
1898
- * Coerce a value to a number if it represents a numeric type
3145
+ * Optional multi-fact merge metadata.
3146
+ * When present, SQL builders execute independent grouped subqueries and
3147
+ * merge them by shared dimension keys.
1899
3148
  */
1900
- private coerceToNumber;
3149
+ multiFactMerge?: {
3150
+ mergeStrategy: 'fullJoin' | 'leftJoin' | 'innerJoin';
3151
+ sharedDimensions: string[];
3152
+ groups: Array<{
3153
+ alias: string;
3154
+ query: SemanticQuery;
3155
+ queryPlan: PhysicalQueryPlan;
3156
+ measures: string[];
3157
+ }>;
3158
+ };
3159
+ /** Warnings about potential query issues (e.g., fan-out without dimensions) */
3160
+ warnings?: QueryWarning[];
3161
+ }
3162
+
3163
+ export declare interface PlanningTrace {
3164
+ /** Ordered decision trace */
3165
+ steps: PlanningTraceStep[];
3166
+ }
3167
+
3168
+ /**
3169
+ * Structured planner trace for explain/dry-run UIs.
3170
+ * Captures key decisions made during logical planning.
3171
+ */
3172
+ export declare interface PlanningTraceStep {
3173
+ /** Pipeline phase name */
3174
+ phase: 'cube_usage' | 'primary_cube_selection' | 'join_planning' | 'cte_planning' | 'measure_strategy' | 'warnings';
3175
+ /** Short summary of what was decided */
3176
+ decision: string;
3177
+ /** Optional machine-readable details */
3178
+ details?: Record<string, unknown>;
3179
+ }
3180
+
3181
+ /** A single optimiser pass that rewrites a logical plan */
3182
+ export declare interface PlanOptimiser {
3183
+ /** Human-readable name for debugging / explain output */
3184
+ readonly name: string;
3185
+ /** Rewrite the plan tree. Must return a valid plan (may return the same reference). */
3186
+ optimise(plan: LogicalNode, context: OptimiserContext): LogicalNode;
3187
+ }
3188
+
3189
+ /**
3190
+ * Type helpers for specific database types
3191
+ */
3192
+ export declare type PostgresDatabase = PostgresJsDatabase<any>;
3193
+
3194
+ export declare class PostgresExecutor extends BaseDatabaseExecutor {
3195
+ execute<T = any[]>(query: SQL | any, numericFields?: string[]): Promise<T>;
3196
+ /**
3197
+ * Convert numeric string fields to numbers (only for measure fields)
3198
+ */
3199
+ private convertNumericFields;
3200
+ /**
3201
+ * Coerce a value to a number if it represents a numeric type
3202
+ */
3203
+ private coerceToNumber;
1901
3204
  getEngineType(): 'postgres';
1902
3205
  /**
1903
3206
  * Execute EXPLAIN on a SQL query to get the execution plan
@@ -1996,6 +3299,38 @@ export declare interface PreAggregationCTEInfo {
1996
3299
  cteReason?: 'hasMany' | 'fanOutPrevention';
1997
3300
  }
1998
3301
 
3302
+ /**
3303
+ * A scored candidate path considered by preferred-path selection.
3304
+ */
3305
+ declare interface PreferredPathCandidateScore {
3306
+ path: InternalJoinPathStep[];
3307
+ score: number;
3308
+ usesPreferredJoin: boolean;
3309
+ preferredCubesInPath: number;
3310
+ usesProcessed: boolean;
3311
+ scoreBreakdown: PreferredPathScoreBreakdown;
3312
+ }
3313
+
3314
+ /**
3315
+ * Score breakdown for a candidate preferred path.
3316
+ */
3317
+ declare interface PreferredPathScoreBreakdown {
3318
+ preferredJoinBonus: number;
3319
+ preferredCubeBonus: number;
3320
+ lengthPenalty: number;
3321
+ }
3322
+
3323
+ /**
3324
+ * Detailed preferred-path selection output for analysis/debug UIs.
3325
+ */
3326
+ declare interface PreferredPathSelection {
3327
+ strategy: 'preferred' | 'fallbackShortest';
3328
+ preferredCubes: string[];
3329
+ selectedIndex: number;
3330
+ candidates: PreferredPathCandidateScore[];
3331
+ selectedPath: InternalJoinPathStep[] | null;
3332
+ }
3333
+
1999
3334
  /**
2000
3335
  * Primary cube selection analysis
2001
3336
  */
@@ -2151,110 +3486,8 @@ export declare interface QueryAnalysis {
2151
3486
  querySummary: QuerySummary;
2152
3487
  /** Warnings or potential issues */
2153
3488
  warnings?: string[];
2154
- }
2155
-
2156
- export declare class QueryBuilder {
2157
- private dateTimeBuilder;
2158
- private filterBuilder;
2159
- private groupByBuilder;
2160
- private measureBuilder;
2161
- constructor(databaseAdapter: DatabaseAdapter);
2162
- /**
2163
- * Build resolvedMeasures map for a set of measures
2164
- * Delegates to MeasureBuilder
2165
- */
2166
- buildResolvedMeasures(measureNames: string[], cubeMap: Map<string, Cube>, context: QueryContext, customMeasureBuilder?: (measureName: string, measure: any, cube: Cube) => SQL): ResolvedMeasures;
2167
- /**
2168
- * Build dynamic selections for measures, dimensions, and time dimensions
2169
- * Works for both single and multi-cube queries
2170
- * Handles calculated measures with dependency resolution
2171
- */
2172
- buildSelections(cubes: Map<string, Cube> | Cube, query: SemanticQuery, context: QueryContext): Record<string, SQL | AnyColumn>;
2173
- /**
2174
- * Build calculated measure expression by substituting {member} references
2175
- * Delegates to MeasureBuilder
2176
- */
2177
- buildCalculatedMeasure(measure: any, cube: Cube, allCubes: Map<string, Cube>, resolvedMeasures: ResolvedMeasures, context: QueryContext): SQL;
2178
- /**
2179
- * Build resolved measures map for a calculated measure from CTE columns
2180
- * Delegates to MeasureBuilder
2181
- */
2182
- buildCTECalculatedMeasure(measure: any, cube: Cube, cteInfo: {
2183
- cteAlias: string;
2184
- measures: string[];
2185
- cube: Cube;
2186
- }, allCubes: Map<string, Cube>, context: QueryContext): SQL;
2187
- /**
2188
- * Build measure expression for HAVING clause, handling CTE references correctly
2189
- * Delegates to MeasureBuilder
2190
- */
2191
- private buildHavingMeasureExpression;
2192
- /**
2193
- * Build measure expression with aggregation and filters
2194
- * Delegates to MeasureBuilder
2195
- */
2196
- buildMeasureExpression(measure: any, context: QueryContext, cube?: Cube): SQL;
2197
- /**
2198
- * Build time dimension expression with granularity using database adapter
2199
- * Delegates to DateTimeBuilder
2200
- */
2201
- buildTimeDimensionExpression(dimensionSql: any, granularity: string | undefined, context: QueryContext): SQL;
2202
- /**
2203
- * Build WHERE conditions from semantic query filters (dimensions only)
2204
- * Works for both single and multi-cube queries
2205
- * @param preBuiltFilters - Optional map of cube name to pre-built filter SQL for parameter deduplication
2206
- */
2207
- buildWhereConditions(cubes: Map<string, Cube> | Cube, query: SemanticQuery, context: QueryContext, queryPlan?: QueryPlan, preBuiltFilters?: Map<string, SQL[]>): SQL[];
2208
- /**
2209
- * Build HAVING conditions from semantic query filters (measures only)
2210
- * Works for both single and multi-cube queries
2211
- */
2212
- buildHavingConditions(cubes: Map<string, Cube> | Cube, query: SemanticQuery, context: QueryContext, queryPlan?: QueryPlan): SQL[];
2213
- /**
2214
- * Process a single filter (basic or logical)
2215
- * @param filterType - 'where' for dimension filters, 'having' for measure filters
2216
- */
2217
- private processFilter;
2218
- /**
2219
- * Build filter condition using Drizzle operators
2220
- * Delegates to FilterBuilder
2221
- */
2222
- private buildFilterCondition;
2223
- /**
2224
- * Build date range condition for time dimensions
2225
- * Delegates to DateTimeBuilder
2226
- */
2227
- buildDateRangeCondition(fieldExpr: AnyColumn | SQL, dateRange: string | string[]): SQL | null;
2228
- /**
2229
- * Build GROUP BY fields from dimensions and time dimensions
2230
- * Delegates to GroupByBuilder
2231
- */
2232
- buildGroupByFields(cubes: Map<string, Cube> | Cube, query: SemanticQuery, context: QueryContext, queryPlan?: any): (SQL | AnyColumn)[];
2233
- /**
2234
- * Build ORDER BY clause with automatic time dimension sorting
2235
- */
2236
- buildOrderBy(query: SemanticQuery, selectedFields?: string[]): SQL[];
2237
- /**
2238
- * Collect numeric field names (measures + numeric dimensions) for type conversion
2239
- * Works for both single and multi-cube queries
2240
- */
2241
- collectNumericFields(cubes: Map<string, Cube> | Cube, query: SemanticQuery): string[];
2242
- /**
2243
- * Apply LIMIT and OFFSET to a query with validation
2244
- * If offset is provided without limit, add a reasonable default limit
2245
- */
2246
- applyLimitAndOffset<T>(query: T, semanticQuery: SemanticQuery): T;
2247
- /**
2248
- * Public wrapper for buildFilterCondition - used by executor for cache preloading
2249
- * This allows pre-building filter SQL before query construction
2250
- */
2251
- buildFilterConditionPublic(fieldExpr: AnyColumn | SQL, operator: FilterOperator, values: any[], field?: any, dateRange?: string | string[]): SQL | null;
2252
- /**
2253
- * Build a logical filter (AND/OR) - used by executor for cache preloading
2254
- * This handles nested filter structures and builds combined SQL
2255
- * Delegates to FilterBuilder
2256
- */
2257
- buildLogicalFilter(filter: Filter, cubes: Map<string, Cube>, context: QueryContext): SQL | null;
3489
+ /** Structured decision trace for debugging and dry-run UIs */
3490
+ planningTrace?: PlanningTrace;
2258
3491
  }
2259
3492
 
2260
3493
  /**
@@ -2275,7 +3508,7 @@ export declare interface QueryCacheMetadata {
2275
3508
  * Query context passed to cube SQL functions
2276
3509
  * Provides access to database, schema, and security context
2277
3510
  */
2278
- declare interface QueryContext {
3511
+ export declare interface QueryContext {
2279
3512
  /** Drizzle database instance */
2280
3513
  db: DrizzleDatabase;
2281
3514
  /** Database schema (tables, columns, etc.) */
@@ -2292,20 +3525,19 @@ declare interface QueryContext {
2292
3525
  */
2293
3526
  filterCache?: FilterCacheManager;
2294
3527
  }
2295
- export { QueryContext }
2296
- export { QueryContext as SemanticQueryContext }
2297
3528
 
2298
3529
  export declare class QueryExecutor {
2299
3530
  private dbExecutor;
2300
3531
  private queryBuilder;
2301
- private queryPlanner;
2302
- private cteBuilder;
3532
+ private drizzlePlanBuilder;
2303
3533
  private databaseAdapter;
2304
3534
  private comparisonQueryBuilder;
2305
3535
  private funnelQueryBuilder;
2306
3536
  private flowQueryBuilder;
2307
3537
  private retentionQueryBuilder;
2308
3538
  private cacheConfig?;
3539
+ private logicalPlanBuilder;
3540
+ private planOptimiser;
2309
3541
  constructor(dbExecutor: DatabaseExecutor, cacheConfig?: CacheConfig);
2310
3542
  /**
2311
3543
  * Unified query execution method that handles both single and multi-cube queries
@@ -2313,411 +3545,180 @@ export declare class QueryExecutor {
2313
3545
  */
2314
3546
  execute(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext, options?: ExecutionOptions): Promise<QueryResult>;
2315
3547
  /**
2316
- * Legacy interface for single cube queries
2317
- */
2318
- executeQuery(cube: Cube, query: SemanticQuery, securityContext: SecurityContext): Promise<QueryResult>;
2319
- /**
2320
- * Execute a comparison query with caching support
2321
- * Wraps executeComparisonQuery with cache set logic
2322
- */
2323
- private executeComparisonQueryWithCache;
2324
- /**
2325
- * Execute a comparison query with multiple date periods
2326
- * Expands compareDateRange into multiple sub-queries and merges results
2327
- */
2328
- private executeComparisonQuery;
2329
- /**
2330
- * Execute a funnel query with caching support
2331
- */
2332
- private executeFunnelQueryWithCache;
2333
- /**
2334
- * Execute a funnel analysis query
2335
- */
2336
- private executeFunnelQuery;
2337
- /**
2338
- * Execute a flow query with caching support
2339
- */
2340
- private executeFlowQueryWithCache;
2341
- /**
2342
- * Execute a flow analysis query
2343
- * Produces Sankey diagram data (nodes and links)
2344
- */
2345
- private executeFlowQuery;
2346
- /**
2347
- * Execute a retention query with caching support
2348
- */
2349
- private executeRetentionQueryWithCache;
2350
- /**
2351
- * Execute a retention analysis query
2352
- * Calculates cohort-based retention rates
2353
- */
2354
- private executeRetentionQuery;
2355
- /**
2356
- * Standard query execution (non-comparison)
2357
- * This is the core execution logic extracted for use by comparison queries
2358
- */
2359
- private executeStandardQuery;
2360
- /**
2361
- * Validate that all cubes in the query plan have proper security filtering.
2362
- * Emits a warning if a cube's sql() function doesn't return a WHERE clause.
2363
- *
2364
- * Security is critical in multi-tenant applications - this validation helps
2365
- * detect cubes that may leak data across tenants.
2366
- */
2367
- private validateSecurityContext;
2368
- /**
2369
- * Build unified query that works for both single and multi-cube queries
2370
- */
2371
- private buildUnifiedQuery;
2372
- /**
2373
- * Convert query plan to cube map for QueryBuilder methods
2374
- */
2375
- private getCubesFromPlan;
2376
- /**
2377
- * For hasMany CTEs, detect when the outer query grain already matches the CTE join key.
2378
- * In that case, re-aggregation should use MAX (not SUM) to avoid multiplying values by
2379
- * lower-grain primary cube rows.
2380
- */
2381
- private shouldUseMaxForHasManyAtJoinKeyGrain;
2382
- /**
2383
- * Checks whether query grouping includes a dimension backed by the given column.
2384
- */
2385
- private queryGroupsByColumn;
2386
- /**
2387
- * Generate raw SQL for debugging (without execution) - unified approach
2388
- */
2389
- generateSQL(cube: Cube, query: SemanticQuery, securityContext: SecurityContext): Promise<{
2390
- sql: string;
2391
- params?: any[];
2392
- }>;
2393
- /**
2394
- * Generate raw SQL for multi-cube queries without execution - unified approach
2395
- */
2396
- generateMultiCubeSQL(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext): Promise<{
2397
- sql: string;
2398
- params?: any[];
2399
- }>;
2400
- /**
2401
- * Generate SQL for a funnel query without execution (dry-run)
2402
- * Returns the actual CTE-based SQL that would be executed
2403
- */
2404
- dryRunFunnel(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext): Promise<{
2405
- sql: string;
2406
- params?: any[];
2407
- }>;
2408
- /**
2409
- * Generate SQL for a flow query without execution (dry-run)
2410
- * Returns the actual CTE-based SQL that would be executed
2411
- */
2412
- dryRunFlow(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext): Promise<{
2413
- sql: string;
2414
- params?: any[];
2415
- }>;
2416
- /**
2417
- * Generate SQL for a retention query without execution (dry-run)
2418
- * Returns the actual CTE-based SQL that would be executed
2419
- */
2420
- dryRunRetention(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext): Promise<{
2421
- sql: string;
2422
- params?: any[];
2423
- }>;
2424
- /**
2425
- * Execute EXPLAIN on a query to get the execution plan
2426
- * Generates the SQL using the same secure path as execute/generateSQL,
2427
- * then runs EXPLAIN on the database.
2428
- */
2429
- explainQuery(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext, options?: ExplainOptions): Promise<ExplainResult>;
2430
- /**
2431
- * Generate SQL using unified approach (works for both single and multi-cube)
2432
- */
2433
- private generateUnifiedSQL;
2434
- /**
2435
- * Generate annotations for UI metadata - unified approach
2436
- */
2437
- private generateAnnotations;
2438
- /**
2439
- * Pre-build filter SQL and store in cache for reuse across CTEs and main query
2440
- * This enables parameter deduplication - the same filter values are shared
2441
- * rather than appearing as separate parameters in different parts of the query
3548
+ * Build a logical plan for a query without executing it.
3549
+ * Useful for testing, debugging, and plan inspection.
2442
3550
  */
2443
- private preloadFilterCache;
3551
+ buildLogicalPlan(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext): QueryNode;
2444
3552
  /**
2445
- * Build post-aggregation window function expression
2446
- *
2447
- * Post-aggregation windows operate on already-aggregated data:
2448
- * 1. The base measure is aggregated (e.g., SUM(revenue))
2449
- * 2. The window function is applied (e.g., LAG(...) OVER ORDER BY date)
2450
- * 3. An optional operation is applied (e.g., current - previous)
2451
- *
2452
- * @param measure - The window function measure definition
2453
- * @param baseMeasureExpr - The aggregated base measure expression
2454
- * @param query - The semantic query (for dimension context)
2455
- * @param context - Query context
2456
- * @param cube - The cube containing this measure
2457
- * @returns SQL expression for the window function
3553
+ * Analyze planning decisions for a regular query using the same logical
3554
+ * planning path as execution and dry-run SQL generation.
2458
3555
  */
2459
- private buildPostAggregationWindowExpression;
2460
- }
2461
-
2462
- /**
2463
- * Unified Query Plan for both single and multi-cube queries
2464
- * - For single-cube queries: joinCubes array is empty
2465
- * - For multi-cube queries: joinCubes contains the additional cubes to join
2466
- * - selections, whereConditions, and groupByFields are populated by QueryBuilder
2467
- */
2468
- export declare interface QueryPlan {
2469
- /** Primary cube that drives the query */
2470
- primaryCube: Cube;
2471
- /** Additional cubes to join (empty for single-cube queries) */
2472
- joinCubes: Array<{
2473
- cube: Cube;
2474
- alias: string;
2475
- joinType: 'inner' | 'left' | 'right' | 'full';
2476
- joinCondition: SQL;
2477
- /** Junction table information for belongsToMany relationships */
2478
- junctionTable?: {
2479
- table: Table;
2480
- alias: string;
2481
- joinType: 'inner' | 'left' | 'right' | 'full';
2482
- joinCondition: SQL;
2483
- /** Optional security SQL function to apply to junction table */
2484
- securitySql?: (securityContext: SecurityContext) => SQL | SQL[];
2485
- /** Source cube name for the belongsToMany relationship (needed for CTE rewriting) */
2486
- sourceCubeName?: string;
2487
- };
2488
- }>;
2489
- /** Combined field selections across all cubes (built by QueryBuilder) */
2490
- selections: Record<string, SQL | AnyColumn>;
2491
- /** WHERE conditions for the entire query (built by QueryBuilder) */
2492
- whereConditions: SQL[];
2493
- /** GROUP BY fields if aggregations are present (built by QueryBuilder) */
2494
- groupByFields: (SQL | AnyColumn)[];
2495
- /** Pre-aggregation CTEs for hasMany relationships to prevent fan-out */
2496
- preAggregationCTEs?: PreAggregationCTEInfo[];
2497
- /** Warnings about potential query issues (e.g., fan-out without dimensions) */
2498
- warnings?: QueryWarning[];
2499
- }
2500
-
2501
- /**
2502
- * Pre-aggregation plan for handling hasMany relationships
2503
- */
2504
- export declare class QueryPlanner {
2505
- private resolverCache;
3556
+ analyzeQuery(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext): QueryAnalysis;
2506
3557
  /**
2507
- * Get or create a JoinPathResolver for the given cubes map
2508
- */
2509
- private getResolver;
2510
- /**
2511
- * Analyze a semantic query to determine which cubes are involved
2512
- */
2513
- analyzeCubeUsage(query: SemanticQuery): Set<string>;
2514
- /**
2515
- * Recursively extract cube names from filters (handles logical filters)
2516
- */
2517
- private extractCubeNamesFromFilter;
2518
- /**
2519
- * Extract measures referenced in filters (for CTE inclusion)
2520
- */
2521
- private extractMeasuresFromFilters;
2522
- /**
2523
- * Recursively extract measures from filters for a specific cube
2524
- * Only includes filter members that are actually measures (not dimensions)
2525
- */
2526
- private extractMeasuresFromFilter;
2527
- /**
2528
- * Create a unified query plan that works for both single and multi-cube queries
2529
- */
2530
- createQueryPlan(cubes: Map<string, Cube>, query: SemanticQuery, ctx: QueryContext): QueryPlan;
2531
- /**
2532
- * Choose the primary cube based on query analysis
2533
- * Uses a consistent strategy to avoid measure order dependencies
2534
- *
2535
- * Delegates to analyzePrimaryCubeSelection() for the actual logic,
2536
- * ensuring a single source of truth for primary cube selection.
2537
- */
2538
- choosePrimaryCube(cubeNames: string[], query: SemanticQuery, cubes?: Map<string, Cube>): string;
2539
- /**
2540
- * Build join plan for multi-cube query
2541
- * Supports both direct joins and transitive joins through intermediate cubes
2542
- *
2543
- * Uses query-aware path selection to prefer joining through cubes that have
2544
- * measures in the query (e.g., joining Teams through EmployeeTeams when
2545
- * EmployeeTeams.count is a measure)
3558
+ * Legacy interface for single cube queries
2546
3559
  */
2547
- private buildJoinPlan;
2548
- /**
2549
- * Plan pre-aggregation CTEs for hasMany relationships to prevent fan-out
2550
- * Note: belongsToMany relationships handle fan-out differently through their junction table structure
2551
- * and don't require CTEs - the two-hop join with the junction table provides natural grouping
2552
- *
2553
- * CRITICAL FAN-OUT PREVENTION LOGIC:
2554
- * When a query contains ANY hasMany relationship in the join graph, ALL cubes with measures
2555
- * that could be affected by row multiplication need CTEs. This includes:
2556
- *
2557
- * 1. Cubes with direct hasMany FROM primary (existing logic)
2558
- * 2. Cubes with measures that would be multiplied due to hasMany elsewhere in the query
2559
- * - Example: Query has Departments.totalBudget + Productivity.recordCount
2560
- * - Employees hasMany → Productivity causes row multiplication
2561
- * - Departments.totalBudget would be inflated without CTE pre-aggregation
3560
+ executeQuery(cube: Cube, query: SemanticQuery, securityContext: SecurityContext): Promise<QueryResult>;
3561
+ /**
3562
+ * Execute a comparison query with caching support
3563
+ * Wraps executeComparisonQuery with cache set logic
2562
3564
  */
2563
- private planPreAggregationCTEs;
3565
+ private executeComparisonQueryWithCache;
2564
3566
  /**
2565
- * Find join information TO a cube (reverse lookup)
2566
- * Used when the primary cube needs a CTE and we need to find how other cubes join to it
3567
+ * Execute a comparison query with multiple date periods
3568
+ * Expands compareDateRange into multiple sub-queries and merges results
2567
3569
  */
2568
- private findJoinInfoToCube;
3570
+ private executeComparisonQuery;
3571
+ private buildComparisonExecutionPlan;
2569
3572
  /**
2570
- * Analyze the join path from primary cube to a target CTE cube.
2571
- * Detects if there are intermediate hasMany relationships that would cause fan-out.
2572
- *
2573
- * Returns information about:
2574
- * - The full join path
2575
- * - Whether there are hasMany relationships ON the path (not just at the end)
2576
- * - Which intermediate tables need to be absorbed into the CTE
2577
- * - The correct join key to use (from primary cube's connection point)
2578
- *
2579
- * @param cubes Map of all registered cubes
2580
- * @param primaryCube The primary cube (FROM clause)
2581
- * @param targetCubeName The CTE cube we're analyzing the path to
2582
- * @param ctx Query context for security filtering
3573
+ * Execute a funnel query with caching support
2583
3574
  */
2584
- private analyzeJoinPathToPrimary;
3575
+ private executeFunnelQueryWithCache;
2585
3576
  /**
2586
- * Detect all hasMany relationships that could affect the query
2587
- *
2588
- * This searches ALL cubes involved in the query (including intermediary cubes)
2589
- * to find any hasMany relationships that could cause row multiplication.
2590
- *
2591
- * Key insight: A hasMany relationship in ANY cube that's part of the join path
2592
- * will cause fan-out. We need to detect hasMany from:
2593
- * 1. The primary cube
2594
- * 2. All join cubes
2595
- * 3. Any intermediate cubes that form the join path
3577
+ * Execute a funnel analysis query
2596
3578
  */
2597
- private detectHasManyInQuery;
3579
+ private executeFunnelQuery;
2598
3580
  /**
2599
- * Determine if and why a cube needs pre-aggregation (CTE)
2600
- *
2601
- * Returns:
2602
- * - 'hasMany': Cube is the TARGET of a hasMany relationship - needs SUM in outer query
2603
- * - 'fanOutPrevention': Cube has measures affected by hasMany elsewhere - needs MAX in outer query
2604
- * - null: No CTE needed
2605
- *
2606
- * Key insight: When ANY hasMany exists in a query, ALL cubes with additive measures (sum, count)
2607
- * that are joined to the hasMany source cube are at risk of inflation.
3581
+ * Execute a flow query with caching support
2608
3582
  */
2609
- private getCTEReason;
3583
+ private executeFlowQueryWithCache;
2610
3584
  /**
2611
- * Find join information for a cube from any cube in the query
2612
- * This extends findHasManyJoinDef to work with any relationship type
2613
- * and to search from any source cube, not just the primary
3585
+ * Execute a flow analysis query
3586
+ * Produces Sankey diagram data (nodes and links)
2614
3587
  */
2615
- private findJoinInfoForCube;
3588
+ private executeFlowQuery;
2616
3589
  /**
2617
- * Find downstream cubes that need join keys included in the CTE.
2618
- *
2619
- * When a query has dimensions from a cube (e.g., Teams.name) and measures from
2620
- * a junction cube (e.g., EmployeeTeams.count), the junction CTE needs to include
2621
- * the join key to the dimension cube (team_id) so the dimension cube can be
2622
- * joined through the CTE instead of via an alternative path.
2623
- *
2624
- * @param cteCube The cube being converted to a CTE (e.g., EmployeeTeams)
2625
- * @param query The semantic query with dimensions and measures
2626
- * @param allCubes Map of all registered cubes
2627
- * @returns Array of downstream join key info for cubes needing join through this CTE
3590
+ * Execute a retention query with caching support
2628
3591
  */
2629
- private findDownstreamJoinKeys;
3592
+ private executeRetentionQueryWithCache;
2630
3593
  /**
2631
- * Expand calculated measures to include their dependencies
3594
+ * Execute a retention analysis query
3595
+ * Calculates cohort-based retention rates
2632
3596
  */
2633
- private expandCalculatedMeasureDependencies;
3597
+ private executeRetentionQuery;
2634
3598
  /**
2635
- * Extract measure references from calculatedSql template
3599
+ * Standard query execution (non-comparison)
3600
+ * This is the core execution logic extracted for use by comparison queries
2636
3601
  */
2637
- private extractDependenciesFromTemplate;
3602
+ private executeStandardQuery;
2638
3603
  /**
2639
- * Find hasMany join definition from primary cube to target cube
3604
+ * Create a query context with optional filter cache.
2640
3605
  */
2641
- private findHasManyJoinDef;
3606
+ private createQueryContext;
2642
3607
  /**
2643
- * Find filters that need to propagate from related cubes to a CTE cube.
2644
- * When cube A has filters and a hasMany relationship to cube B (the CTE cube),
2645
- * A's filters should propagate into B's CTE via a subquery.
2646
- *
2647
- * Example: Employees.createdAt filter should propagate to Productivity CTE
2648
- * via: employee_id IN (SELECT id FROM employees WHERE created_at >= $date)
3608
+ * Normalize engine type for optimiser passes.
3609
+ * SingleStore follows MySQL SQL semantics for planner choices.
2649
3610
  */
2650
- private findPropagatingFilters;
3611
+ private getOptimiserEngineType;
2651
3612
  /**
2652
- * Extract cube names from filters into a Set (helper for findPropagatingFilters)
3613
+ * Shared regular-query planning pipeline used by execute, dry-run SQL,
3614
+ * and analysis. This is the single source of planning truth.
2653
3615
  */
2654
- private extractFilterCubeNamesToSet;
3616
+ private buildRegularQueryArtifacts;
2655
3617
  /**
2656
- * Extract filters for a specific cube from the filter array
3618
+ * Validate that all cubes in the query plan have proper security filtering.
3619
+ * Emits a warning if a cube's sql() function doesn't return a WHERE clause.
2657
3620
  *
2658
- * Logic for preserving filter semantics:
2659
- * - AND: Safe to extract only matching branches (AND of fewer conditions is more permissive)
2660
- * - OR: Must include ALL branches or skip entirely (partial OR changes semantics)
2661
- * If any branch belongs to another cube, skip the entire OR to be safe
2662
- * since we can't evaluate the other cube's conditions
3621
+ * Security is critical in multi-tenant applications - this validation helps
3622
+ * detect cubes that may leak data across tenants.
2663
3623
  */
2664
- private extractFiltersForCube;
3624
+ private validateSecurityContext;
2665
3625
  /**
2666
- * Check if all simple filters in a filter array belong to the specified cube
2667
- * Recursively checks nested AND/OR filters
3626
+ * Generate raw SQL for debugging (without execution) - unified approach
2668
3627
  */
2669
- private allFiltersFromCube;
3628
+ generateSQL(cube: Cube, query: SemanticQuery, securityContext: SecurityContext): Promise<{
3629
+ sql: string;
3630
+ params?: any[];
3631
+ }>;
2670
3632
  /**
2671
- * Extract time dimension date range filters as regular filters for a specific cube
3633
+ * Generate raw SQL for multi-cube queries without execution - unified approach
2672
3634
  */
2673
- private extractTimeDimensionFiltersForCube;
3635
+ generateMultiCubeSQL(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext): Promise<{
3636
+ sql: string;
3637
+ params?: any[];
3638
+ }>;
2674
3639
  /**
2675
- * Analyze query planning decisions without building the full query
2676
- * Returns detailed metadata about how the query plan would be constructed
2677
- * Used for debugging and transparency in the playground UI
3640
+ * Generate SQL for a funnel query without execution (dry-run)
3641
+ * Returns the actual CTE-based SQL that would be executed
2678
3642
  */
2679
- analyzeQueryPlan(cubes: Map<string, Cube>, query: SemanticQuery, _ctx: QueryContext): QueryAnalysis;
3643
+ dryRunFunnel(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext): Promise<{
3644
+ sql: string;
3645
+ params?: any[];
3646
+ }>;
2680
3647
  /**
2681
- * Analyze why a particular cube was chosen as primary
3648
+ * Generate SQL for a flow query without execution (dry-run)
3649
+ * Returns the actual CTE-based SQL that would be executed
2682
3650
  */
2683
- private analyzePrimaryCubeSelection;
3651
+ dryRunFlow(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext): Promise<{
3652
+ sql: string;
3653
+ params?: any[];
3654
+ }>;
2684
3655
  /**
2685
- * Analyze the join path between two cubes with detailed step information
2686
- *
2687
- * Uses JoinPathResolver.findPath() for the actual path finding,
2688
- * then converts the result to human-readable analysis format.
3656
+ * Generate SQL for a retention query without execution (dry-run)
3657
+ * Returns the actual CTE-based SQL that would be executed
2689
3658
  */
2690
- private analyzeJoinPath;
3659
+ dryRunRetention(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext): Promise<{
3660
+ sql: string;
3661
+ params?: any[];
3662
+ }>;
2691
3663
  /**
2692
- * Analyze pre-aggregation requirements for hasMany relationships
2693
- * This mirrors the logic in planPreAggregationCTEs to ensure analysis matches execution
3664
+ * Execute EXPLAIN on a query to get the execution plan
3665
+ * Generates the SQL using the same secure path as execute/generateSQL,
3666
+ * then runs EXPLAIN on the database.
2694
3667
  */
2695
- private analyzePreAggregations;
3668
+ explainQuery(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext, options?: ExplainOptions): Promise<ExplainResult>;
2696
3669
  /**
2697
- * Generate warnings for query edge cases that users should be aware of.
2698
- * Currently detects:
2699
- * - FAN_OUT_NO_DIMENSIONS: Query has hasMany CTEs but no dimensions to group by
2700
- *
2701
- * Note: AVG measures in hasMany CTEs can produce mathematically imprecise results
2702
- * (average of averages vs weighted average), but this warning was removed as it
2703
- * fired too aggressively. The issue only occurs when the outer grouping is coarser
2704
- * than the CTE grouping, which is rare in practice. The limitation is documented
2705
- * in executor.ts comments.
3670
+ * Generate SQL for any query mode without execution.
3671
+ * This is the canonical dry-run SQL entrypoint used by explain/adapters.
2706
3672
  */
2707
- private generateWarnings;
3673
+ dryRunSQL(cubes: Map<string, Cube>, query: SemanticQuery, securityContext: SecurityContext): Promise<{
3674
+ sql: string;
3675
+ params?: any[];
3676
+ }>;
2708
3677
  /**
2709
- * Detect when a query has measures from multiple cubes with hasMany relationships
2710
- * but no dimensions to provide grouping context.
2711
- *
2712
- * This is an edge case where:
2713
- * - Query has measures from 2+ cubes
2714
- * - At least one CTE exists (indicating hasMany relationship)
2715
- * - Query has NO dimensions AND NO time dimensions with granularity
2716
- *
2717
- * The SQL is technically correct (CTEs with GROUP BY on join keys), but users
2718
- * may be confused by the aggregated results without visible grouping.
3678
+ * Generate SQL using unified approach (works for both single and multi-cube)
2719
3679
  */
2720
- private checkFanOutNoDimensions;
3680
+ private generateUnifiedSQL;
3681
+ private resolveQueryMode;
3682
+ private validateQueryForMode;
3683
+ private executeQueryByModeWithCache;
3684
+ private executeRegularQueryWithCache;
3685
+ private cacheResult;
3686
+ private generateSqlForMode;
3687
+ private generateComparisonSQL;
3688
+ /**
3689
+ * Generate annotations for UI metadata - unified approach
3690
+ */
3691
+ private generateAnnotations;
3692
+ /**
3693
+ * Pre-build filter SQL and store in cache for reuse across CTEs and main query
3694
+ * This enables parameter deduplication - the same filter values are shared
3695
+ * rather than appearing as separate parameters in different parts of the query
3696
+ */
3697
+ private preloadFilterCache;
3698
+ }
3699
+
3700
+ /**
3701
+ * Root query node — represents the outermost SELECT.
3702
+ * Wraps a source node with dimensions, measures, filters, ordering, etc.
3703
+ */
3704
+ export declare interface QueryNode extends LogicalNodeBase {
3705
+ readonly type: 'query';
3706
+ /** Source of rows (SimpleSource, FullKeyAggregate, etc.) */
3707
+ source: LogicalNode;
3708
+ /** Dimensions to group by */
3709
+ dimensions: DimensionRef[];
3710
+ /** Measures to aggregate */
3711
+ measures: MeasureRef[];
3712
+ /** User-supplied filters */
3713
+ filters: Filter[];
3714
+ /** Time dimensions with granularity / date range */
3715
+ timeDimensions: TimeDimensionRef[];
3716
+ /** ORDER BY clauses */
3717
+ orderBy: OrderByRef[];
3718
+ limit?: number;
3719
+ offset?: number;
3720
+ /** Planning warnings surfaced to the caller */
3721
+ warnings: QueryWarning[];
2721
3722
  }
2722
3723
 
2723
3724
  /**
@@ -2768,6 +3769,8 @@ export declare interface QuerySuggestion {
2768
3769
  export declare interface QuerySummary {
2769
3770
  /** Query type: 'single_cube', 'multi_cube_join', or 'multi_cube_cte' */
2770
3771
  queryType: 'single_cube' | 'multi_cube_join' | 'multi_cube_cte';
3772
+ /** Strategy selected for multiplied-measure handling / multi-fact merge */
3773
+ measureStrategy?: 'simple' | 'keysDeduplication' | 'ctePreAggregateFallback' | 'multiFactMerge';
2771
3774
  /** Total number of joins */
2772
3775
  joinCount: number;
2773
3776
  /** Total number of CTEs */
@@ -2861,6 +3864,104 @@ export declare interface RetentionDateRange {
2861
3864
  end: string;
2862
3865
  }
2863
3866
 
3867
+ export declare class RetentionQueryBuilder {
3868
+ private databaseAdapter;
3869
+ private filterBuilder;
3870
+ private dateTimeBuilder;
3871
+ constructor(databaseAdapter: DatabaseAdapter);
3872
+ /**
3873
+ * Check if query contains retention configuration
3874
+ */
3875
+ hasRetention(query: SemanticQuery): boolean;
3876
+ /**
3877
+ * Validate retention configuration against registered cubes
3878
+ */
3879
+ validateConfig(config: RetentionQueryConfig, cubes: Map<string, Cube>): {
3880
+ isValid: boolean;
3881
+ errors: string[];
3882
+ };
3883
+ /**
3884
+ * Build the retention SQL query using CTEs
3885
+ *
3886
+ * CTE Structure (Simplified Mixpanel-style):
3887
+ * 1. cohort_base - Users entering the cohort (first event in date range)
3888
+ * - When breakdown is specified, includes breakdown_value
3889
+ * 2. activity_periods - All activity with period_number relative to cohort entry
3890
+ * 3. cohort_sizes - Aggregate cohort sizes (per breakdown value if applicable)
3891
+ * 4. retention_counts - Retained users per period (and breakdown value)
3892
+ * 5. Final SELECT - Join with retention rate calculation
3893
+ */
3894
+ buildRetentionQuery(config: RetentionQueryConfig, cubes: Map<string, Cube>, context: QueryContext): any;
3895
+ /**
3896
+ * Transform raw SQL results to RetentionResultRow[]
3897
+ */
3898
+ transformResult(rawResult: unknown[], config: RetentionQueryConfig): RetentionResultRow[];
3899
+ /**
3900
+ * Resolve retention configuration with SQL expressions
3901
+ * Same cube/dimension used for both cohort entry and activity detection
3902
+ */
3903
+ private resolveConfig;
3904
+ /**
3905
+ * Resolve binding key expression for a cube
3906
+ */
3907
+ private resolveBindingKey;
3908
+ /**
3909
+ * Build filter conditions from config filters
3910
+ */
3911
+ private buildFilterConditions;
3912
+ /**
3913
+ * Build a single filter condition
3914
+ */
3915
+ private buildSingleFilterCondition;
3916
+ /**
3917
+ * Build cohort_base CTE
3918
+ * Groups users by their first activity (cohort entry) within the date range.
3919
+ * When breakdowns are specified, includes breakdown values for each dimension.
3920
+ */
3921
+ private buildCohortBaseCTE;
3922
+ /**
3923
+ * Build date range condition for WHERE clause
3924
+ * Filters records to those within the specified date range
3925
+ */
3926
+ private buildDateRangeCondition;
3927
+ /**
3928
+ * Build date range condition for HAVING clause
3929
+ * Used to filter aggregated cohort_period values
3930
+ */
3931
+ private buildDateRangeHavingCondition;
3932
+ /**
3933
+ * Build activity_periods CTE
3934
+ * Joins activity events to cohort_base and calculates period_number.
3935
+ * Includes breakdown values from cohort_base when breakdowns are specified.
3936
+ */
3937
+ private buildActivityPeriodsCTE;
3938
+ /**
3939
+ * Build cohort_sizes CTE
3940
+ * Aggregates the size of the cohort (or per breakdown combination if specified).
3941
+ */
3942
+ private buildCohortSizesCTE;
3943
+ /**
3944
+ * Build retention_counts CTE
3945
+ * Aggregates retained users per period (and breakdown combination if specified).
3946
+ */
3947
+ private buildRetentionCountsCTE;
3948
+ /**
3949
+ * Build rolling retention counts query
3950
+ * For rolling retention, a user is retained in period N if they were active
3951
+ * in period N or any later period
3952
+ */
3953
+ private buildRollingRetentionCountsQuery;
3954
+ /**
3955
+ * Build period number expression using database-specific DATE_DIFF
3956
+ */
3957
+ private buildPeriodNumberExpression;
3958
+ /**
3959
+ * Extract dimension name from a dimension reference
3960
+ * Handles both 'CubeName.dimName' and just 'dimName' formats
3961
+ */
3962
+ private extractDimensionName;
3963
+ }
3964
+
2864
3965
  /**
2865
3966
  * Retention query configuration (Simplified Mixpanel-style format)
2866
3967
  *
@@ -2954,6 +4055,40 @@ export declare interface RetentionTimeDimensionMapping {
2954
4055
  dimension: string;
2955
4056
  }
2956
4057
 
4058
+ /**
4059
+ * A link (edge) in the Sankey diagram
4060
+ * Represents a transition between two nodes
4061
+ */
4062
+ declare interface SankeyLink {
4063
+ /** Source node ID */
4064
+ source: string;
4065
+ /** Target node ID */
4066
+ target: string;
4067
+ /** Count of entities that follow this path */
4068
+ value: number;
4069
+ }
4070
+
4071
+ /**
4072
+ * A node in the Sankey diagram
4073
+ * Represents an event type at a specific layer (distance from starting step)
4074
+ */
4075
+ declare interface SankeyNode {
4076
+ /**
4077
+ * Unique identifier for this node
4078
+ * Format: "before_{depth}_{eventType}" or "after_{depth}_{eventType}" or "start_{eventType}"
4079
+ */
4080
+ id: string;
4081
+ /** Display name (typically the event type value) */
4082
+ name: string;
4083
+ /**
4084
+ * Layer position in the Sankey diagram
4085
+ * Negative for steps before starting step, 0 for starting step, positive for after
4086
+ */
4087
+ layer: number;
4088
+ /** Total count of entities passing through this node */
4089
+ value?: number;
4090
+ }
4091
+
2957
4092
  /**
2958
4093
  * Security context passed to cube SQL functions
2959
4094
  * Contains user/tenant-specific data for filtering
@@ -2991,6 +4126,18 @@ export declare class SemanticLayerCompiler {
2991
4126
  * Check if database executor is configured
2992
4127
  */
2993
4128
  hasExecutor(): boolean;
4129
+ /**
4130
+ * Get configured executor or throw.
4131
+ */
4132
+ private requireExecutor;
4133
+ /**
4134
+ * Create a query executor with optional cache integration.
4135
+ */
4136
+ private createQueryExecutor;
4137
+ /**
4138
+ * Format SQL result using current engine dialect.
4139
+ */
4140
+ private formatSqlResult;
2994
4141
  /**
2995
4142
  * Register a simplified cube with dynamic query building
2996
4143
  * Validates calculated measures during registration
@@ -3063,6 +4210,13 @@ export declare class SemanticLayerCompiler {
3063
4210
  sql: string;
3064
4211
  params?: any[];
3065
4212
  }>;
4213
+ /**
4214
+ * Canonical dry-run SQL generation entrypoint for all query modes.
4215
+ */
4216
+ dryRun(query: SemanticQuery, securityContext: SecurityContext): Promise<{
4217
+ sql: string;
4218
+ params?: any[];
4219
+ }>;
3066
4220
  /**
3067
4221
  * Get SQL for a funnel query without executing it (debugging)
3068
4222
  * Returns the actual CTE-based SQL that would be executed for funnel queries
@@ -3237,6 +4391,20 @@ export declare interface SemanticQuery {
3237
4391
  retention?: RetentionQueryConfig;
3238
4392
  }
3239
4393
 
4394
+ /**
4395
+ * Simple source: one primary cube optionally joined to other cubes.
4396
+ * This is the runtime physical-source shape used by SQL generation today.
4397
+ */
4398
+ export declare interface SimpleSource extends LogicalNodeBase {
4399
+ readonly type: 'simpleSource';
4400
+ /** Primary cube (FROM clause) */
4401
+ primaryCube: CubeRef;
4402
+ /** Cubes joined to the primary */
4403
+ joins: JoinRef[];
4404
+ /** Pre-aggregation CTEs attached to this source */
4405
+ ctes: CTEPreAggregate[];
4406
+ }
4407
+
3240
4408
  export { SQL }
3241
4409
 
3242
4410
  /**
@@ -3394,6 +4562,18 @@ export declare interface TimeDimensionAnnotation {
3394
4562
  granularity?: TimeGranularity;
3395
4563
  }
3396
4564
 
4565
+ /** Reference to a time dimension with granularity/dateRange */
4566
+ declare interface TimeDimensionRef {
4567
+ /** Fully qualified dimension name */
4568
+ name: string;
4569
+ cube: CubeRef;
4570
+ localName: string;
4571
+ granularity?: TimeGranularity;
4572
+ dateRange?: string | string[];
4573
+ fillMissingDates?: boolean;
4574
+ compareDateRange?: (string | [string, string])[];
4575
+ }
4576
+
3397
4577
  export declare type TimeGranularity = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
3398
4578
 
3399
4579
  /**