@@ -4,7 +4,7 @@ import { camelize } from 'inflection';
44
55import { UserError } from './UserError' ;
66import { DynamicReference } from './DynamicReference' ;
7- import { camelizeCube , topologicalSort } from './utils' ;
7+ import { camelizeCube , findCyclesInGraph , topologicalSort } from './utils' ;
88import { BaseQuery } from '../adapter' ;
99
1010import type { ErrorReporter } from './ErrorReporter' ;
@@ -47,19 +47,11 @@ export const CURRENT_CUBE_CONSTANTS = ['CUBE', 'TABLE'];
4747
4848export type CubeDef = any ;
4949
50- export type GraphNode = {
51- cubeDef : CubeDef ;
52- name : string ;
53- } ;
54-
55- export type ReferenceNode = {
56- name : string ;
57- } ;
58-
5950/**
60- * Treat it as GraphNode depends on ReferenceNode
51+ * Tuple of 2 node names
52+ * Treat it as first node depends on the last
6153 */
62- export type GraphEdge = [ GraphNode , ReferenceNode ? ] ;
54+ export type GraphEdge = [ string , string ] ;
6355
6456export class CubeSymbols {
6557 public symbols : Record < string | symbol , any > ;
@@ -115,10 +107,20 @@ export class CubeSymbols {
115107 }
116108 }
117109
118- private prepareDepsGraph ( cubes : CubeDefinition [ ] ) : GraphEdge [ ] {
119- const graph = new Map < string , GraphEdge > ( ) ;
110+ private prepareDepsGraph ( cubes : CubeDefinition [ ] ) : [ Map < string , CubeDef > , GraphEdge [ ] ] {
111+ const graphNodes = new Map < string , CubeDef > ( ) ;
112+ const adjacencyList = new Map < string , Set < string > > ( ) ; // To search for cycles
113+
114+ const addEdge = ( from : string , to : string ) => {
115+ if ( ! adjacencyList . has ( from ) ) {
116+ adjacencyList . set ( from , new Set ( ) ) ;
117+ }
118+ adjacencyList . get ( from ) ! . add ( to ) ;
119+ } ;
120120
121121 for ( const cube of cubes ) {
122+ graphNodes . set ( cube . name , cube ) ;
123+
122124 if ( cube . isView ) {
123125 cube . cubes ?. forEach ( c => {
124126 const jp = c . joinPath || c . join_path ; // View is not camelized yet
@@ -134,27 +136,71 @@ export class CubeSymbols {
134136 [ cubeJoinPath ] = res . split ( '.' ) ;
135137 }
136138 }
137- graph . set ( ` ${ cube . name } - ${ cubeJoinPath } ` , [ { cubeDef : cube , name : cube . name } , { name : cubeJoinPath } ] ) ;
139+ addEdge ( cube . name , cubeJoinPath ) ;
138140 }
139141 } ) ;
140142
141143 // Legacy-style includes
142144 if ( typeof cube . includes === 'function' ) {
143145 const refs = this . funcArguments ( cube . includes ) ;
144146 refs . forEach ( ref => {
145- graph . set ( ` ${ cube . name } - ${ ref } ` , [ { cubeDef : cube , name : cube . name } , { name : ref } ] ) ;
147+ addEdge ( cube . name , ref ) ;
146148 } ) ;
147149 }
148150 } else if ( cube . joins && Object . keys ( cube . joins ) . length > 0 ) {
149151 Object . keys ( cube . joins ) . forEach ( j => {
150- graph . set ( ` ${ cube . name } - ${ j } ` , [ { cubeDef : cube , name : cube . name } , { name : j } ] ) ;
152+ addEdge ( cube . name , j ) ;
151153 } ) ;
152154 } else {
153- graph . set ( `${ cube . name } -none` , [ { cubeDef : cube , name : cube . name } ] ) ;
155+ adjacencyList . set ( cube . name , new Set ( ) ) ;
156+ }
157+ }
158+
159+ const cycles = findCyclesInGraph ( adjacencyList ) ;
160+
161+ for ( const cycle of cycles ) {
162+ const cycleSet = new Set ( cycle ) ;
163+
164+ // Validate that cycle doesn't have views
165+ if ( cycle . some ( node => graphNodes . get ( node ) ?. isView ) ) {
166+ throw new UserError ( `A view cannot be part of a dependency loop. Please review your cube definitions ${ cycle . join ( ', ' ) } and ensure that no views are included in loops.` ) ;
167+ }
168+
169+ // Let's find external dependencies (who refers to the loop)
170+ const externalNodes = new Set < string > ( ) ;
171+ for ( const [ from , toSet ] of adjacencyList . entries ( ) ) {
172+ if ( ! cycleSet . has ( from ) ) {
173+ for ( const to of toSet ) {
174+ if ( cycleSet . has ( to ) ) {
175+ externalNodes . add ( from ) ;
176+ }
177+ }
178+ }
179+ }
180+
181+ // Remove all edges inside the loop
182+ for ( const node of cycle ) {
183+ adjacencyList . set ( node , new Set ( [ ...adjacencyList . get ( node ) ! ] . filter ( n => ! cycleSet . has ( n ) ) ) ) ;
184+ }
185+
186+ // If there are external dependencies, point them to every node in the loop
187+ if ( externalNodes . size > 0 ) {
188+ for ( const external of externalNodes ) {
189+ for ( const cube of cycle ) {
190+ addEdge ( external , cube ) ;
191+ }
192+ }
193+ }
194+ }
195+
196+ const graphEdges : GraphEdge [ ] = [ ] ;
197+ for ( const [ from , toSet ] of adjacencyList ) {
198+ for ( const to of toSet ) {
199+ graphEdges . push ( [ from , to ] ) ;
154200 }
155201 }
156202
157- return Array . from ( graph . values ( ) ) ;
203+ return [ graphNodes , graphEdges ] ;
158204 }
159205
160206 public getCubeDefinition ( cubeName : string ) {
0 commit comments