diff --git a/meerkat-browser/package.json b/meerkat-browser/package.json index be6ba597..59732dcc 100644 --- a/meerkat-browser/package.json +++ b/meerkat-browser/package.json @@ -1,6 +1,6 @@ { "name": "@devrev/meerkat-browser", - "version": "0.0.72", + "version": "0.0.73", "dependencies": { "@swc/helpers": "~0.5.0", "@devrev/meerkat-core": "*", diff --git a/meerkat-core/package.json b/meerkat-core/package.json index 8efc4ab7..fb47a2d6 100644 --- a/meerkat-core/package.json +++ b/meerkat-core/package.json @@ -1,6 +1,6 @@ { "name": "@devrev/meerkat-core", - "version": "0.0.74", + "version": "0.0.75", "dependencies": { "@swc/helpers": "~0.5.0" }, diff --git a/meerkat-core/src/joins/joins.spec.ts b/meerkat-core/src/joins/joins.spec.ts index 685be70e..b68231de 100644 --- a/meerkat-core/src/joins/joins.spec.ts +++ b/meerkat-core/src/joins/joins.spec.ts @@ -1,5 +1,5 @@ import { - checkLoopInGraph, + checkLoopInJoinPath, createDirectedGraph, generateSqlQuery, } from './joins'; @@ -60,32 +60,6 @@ describe('Table schema functions', () => { }); }); - it('should correctly identify if a loop exists in the graph', () => { - const graph = { - table1: { - table2: { - field1: 'table2.field3 = table1.field4', - }, - table3: { - field2: 'table3.field5 = table1.field2', - }, - }, - table2: { - table3: { - field3: 'table3.field4 = table2.field3', - }, - }, - table3: { - table1: { - field5: 'table1.field1 = table3.field2', - }, - }, - }; - const hasLoop = checkLoopInGraph(graph); - - expect(hasLoop).toBe(true); - }); - it('should correctly generate a SQL query from the provided join path, table schema SQL map, and directed graph', () => { const joinPaths = [ [ @@ -113,21 +87,34 @@ describe('Table schema functions', () => { ); }); - it('should throw an error when a cycle exists in checkLoopInGraph', () => { - const graph = { - node1: { node2: { id: 'node1.id = node2.id' } }, - node2: { node3: { id: 'node2.id = node3.id ' } }, - node3: { node1: { id: 'node3.id = node1.id' } }, - }; - const output = checkLoopInGraph(graph); - expect(output).toBe(true); - }); - - it('checkLoopInGraph should return false for disconnected graph', () => { - const graph = { - node1: { node2: { id: 'node1.id = node2.id ' } }, - node3: { node4: { id: 'node3.id = node4.id ' } }, - }; - expect(checkLoopInGraph(graph)).toBe(false); - }); + describe('checkLoopInJoinPath', () => { + it('should return false if there is no loop in the join path', () => { + const joinPath = [ + [ + { left: 'table1', right: 'table2', on: 'id' }, + { left: 'table2', right: 'table3', on: 'id' }, + ], + ]; + expect(checkLoopInJoinPath(joinPath)).toBe(false); + }) + it('should return true if there is a loop in the join path', () => { + const joinPath = [ + [ + { left: 'table1', right: 'table2', on: 'id' }, + { left: 'table2', right: 'table3', on: 'id' }, + { left: 'table3', right: 'table1', on: 'id' }, + ], + ]; + expect(checkLoopInJoinPath(joinPath)).toBe(true); + }) + it('should return false for single node', () => { + const joinPath = [ + [ + { left: 'table1', }, + { left: 'table1' }, + ], + ]; + expect(checkLoopInJoinPath(joinPath)).toBe(false); + }) + }) }); diff --git a/meerkat-core/src/joins/joins.ts b/meerkat-core/src/joins/joins.ts index 30c4e0b9..a8f9e9be 100644 --- a/meerkat-core/src/joins/joins.ts +++ b/meerkat-core/src/joins/joins.ts @@ -57,11 +57,9 @@ export function generateSqlQuery( // If visitedFrom is undefined, this is the first visit to the node visitedNodes.set(currentEdge.right, currentEdge); - query += ` LEFT JOIN (${tableSchemaSqlMap[currentEdge.right]}) AS ${ - currentEdge.right - } ON ${ - directedGraph[currentEdge.left][currentEdge.right][currentEdge.on] - }`; + query += ` LEFT JOIN (${tableSchemaSqlMap[currentEdge.right]}) AS ${currentEdge.right + } ON ${directedGraph[currentEdge.left][currentEdge.right][currentEdge.on] + }`; } } @@ -152,38 +150,23 @@ export const createDirectedGraph = ( return directedGraph; }; -function DFS( - graph: any, - node: string, - visited: Set, - recStack: Set -): boolean { - visited.add(node); - recStack.add(node); - - for (const neighbor in graph[node]) { - if (!visited.has(neighbor) && DFS(graph, neighbor, visited, recStack)) { - return true; - } else if (recStack.has(neighbor)) { - return true; - } - } - - recStack.delete(node); - return false; -} -export function checkLoopInGraph(graph: any): boolean { - const visited = new Set(); - const recStack = new Set(); - - for (const node in graph) { - if (DFS(graph, node, visited, recStack)) { - return true; +export const checkLoopInJoinPath = (joinPath: JoinPath[]) => { + for (let i = 0; i < joinPath.length; i++) { + const visitedNodes = new Set(); + const currentJoinPath = joinPath[i]; + visitedNodes.add(currentJoinPath[0].left); + for (let j = 0; j < currentJoinPath.length; j++) { + const currentEdge = currentJoinPath[j]; + if (isJoinNode(currentEdge) && visitedNodes.has(currentEdge.right)) { + if (visitedNodes.has(currentEdge.right)) { + return true; + } + visitedNodes.add(currentEdge.right); + } } } - - return false; + return false } export const getCombinedTableSchema = async ( @@ -202,9 +185,9 @@ export const getCombinedTableSchema = async ( ); const directedGraph = createDirectedGraph(tableSchema, tableSchemaSqlMap); - const hasLoop = checkLoopInGraph(directedGraph); + const hasLoop = checkLoopInJoinPath(cubeQuery.joinPaths || []); if (hasLoop) { - throw new Error('A loop was detected in the joins.'); + throw new Error(`A loop was detected in the joins. ${JSON.stringify(cubeQuery.joinPaths || [])}`); } const baseSql = generateSqlQuery( diff --git a/meerkat-core/src/utils/__fixtures__/joins.fixtures.ts b/meerkat-core/src/utils/__fixtures__/joins.fixtures.ts new file mode 100644 index 00000000..06dd32e8 --- /dev/null +++ b/meerkat-core/src/utils/__fixtures__/joins.fixtures.ts @@ -0,0 +1,844 @@ + +export const CIRCULAR_TABLE_SCHEMA = [ + { + name: 'node1', + dimensions: [ + { + name: 'id', + sql: 'node1.id', + }, + ], + measures: [], + sql: 'select * from node1', + joins: [ + { sql: 'node1.id = node2.id' }, + { sql: 'node1.id = node3.id' }, + ], + }, + { + name: 'node2', + dimensions: [ + { + name: 'id', + sql: 'node2.id', + }, + { + name: 'node11_id', + sql: 'node2.node11_id', + }, + ], + measures: [], + sql: 'select * from node2', + joins: [ + { sql: 'node2.id = node3.id' }, + { sql: 'node2.id = node4.id' }, + ], + }, + { + name: 'node3', + dimensions: [ + { + name: 'id', + sql: 'node3.id', + }, + ], + measures: [], + sql: 'select * from node3', + joins: [ + { sql: 'node3.id = node4.id' }, + { sql: 'node3.id = node1.id' } + ], + }, + { + name: 'node4', + dimensions: [ + { + name: 'id', + sql: 'node4.id', + }, + ], + measures: [], + sql: 'select * from node4', + joins: [], + }, +]; + +export const LINEAR_TABLE_SCHEMA = [ + { + name: 'node1', + dimensions: [ + { + name: 'id', + sql: 'node1.id', + }, + ], + measures: [], + sql: 'select * from node1', + joins: [ + { sql: 'node1.id = node2.id' }, + { sql: 'node1.id = node3.id' }, + { sql: 'node1.id = node6.id' }, + ], + }, + { + name: 'node2', + dimensions: [ + { + name: 'id', + sql: 'node2.id', + }, + { + name: 'node11_id', + sql: 'node2.node11_id', + }, + ], + measures: [], + sql: 'select * from node2', + joins: [ + { sql: 'node2.id = node4.id' }, + { sql: 'node2.node11_id = node11.id' }, + ], + }, + { + name: 'node3', + dimensions: [ + { + name: 'id', + sql: 'node3.id', + }, + ], + measures: [], + sql: 'select * from node3', + joins: [{ sql: 'node3.id = node5.id' }], + }, + { + name: 'node4', + dimensions: [ + { + name: 'id', + sql: 'node4.id', + }, + ], + measures: [], + sql: 'select * from node4', + joins: [ + { sql: 'node4.id = node5.id' }, + { sql: 'node4.id = node6.id' }, + { sql: 'node4.id = node7.id' }, + ], + }, + { + name: 'node5', + measures: [], + dimensions: [ + { + name: 'id', + sql: 'node5.id', + }, + ], + sql: 'select * from node5', + joins: [{ sql: 'node5.id = node8.id' }], + }, + { + name: 'node6', + dimensions: [ + { + name: 'id', + sql: 'node6.id', + }, + ], + measures: [], + sql: 'select * from node6', + joins: [{ sql: 'node6.id = node9.id' }], + }, + { + name: 'node7', + dimensions: [ + { + name: 'id', + sql: 'node7.id', + }, + ], + measures: [], + sql: 'select * from node7', + joins: [{ sql: 'node7.id = node10.id' }], + }, + { + name: 'node8', + dimensions: [ + { + name: 'id', + sql: 'node8.id', + }, + ], + measures: [], + sql: 'select * from node8', + joins: [], + }, + { + name: 'node9', + dimensions: [ + { + name: 'id', + sql: 'node9.id', + }, + ], + measures: [], + sql: 'select * from node9', + joins: [{ sql: 'node9.id = node10.id' }], + }, + { + name: 'node10', + dimensions: [ + { + name: 'id', + sql: 'node10.id', + }, + ], + measures: [], + sql: 'select * from node10', + joins: [], + }, + { + name: 'node11', + dimensions: [ + { + name: 'id', + sql: 'node11.id', + }, + ], + measures: [], + sql: 'select * from node11', + joins: [], + }, +]; + +export const BASIC_JOIN_PATH = [ + [ + { + left: 'node1', + right: 'node2', + on: 'id', + }, + ], +]; + +export const SINGLE_NODE_JOIN_PATH = [ + [ + { + left: 'node1', + }, + ], +]; + +export const INTERMEDIATE_JOIN_PATH = [ + [ + { + left: 'node1', + right: 'node2', + on: 'id', + }, + { + left: 'node2', + right: 'node4', + on: 'id', + }, + ], + [ + { + left: 'node1', + right: 'node3', + on: 'id', + }, + ], +]; + +export const CIRCULAR_JOIN_PATH = [ + [ + { + left: 'node1', + right: 'node3', + on: 'id', + }, + ], + [ + { + left: 'node1', + right: 'node2', + on: 'id', + }, + { + left: 'node2', + right: 'node4', + on: 'id', + }, + { + left: 'node4', + right: 'node1', + on: 'id', + }, + ], + +]; + + +export const COMPLEX_JOIN_PATH = [ + [ + { + left: 'node1', + right: 'node2', + on: 'id', + }, + { + left: 'node2', + right: 'node4', + on: 'id', + }, + { + left: 'node4', + right: 'node7', + on: 'id', + }, + ], + [ + { + left: 'node1', + right: 'node3', + on: 'id', + }, + ], + [ + { + left: 'node1', + right: 'node2', + on: 'id', + }, + { + left: 'node2', + right: 'node11', + on: 'node11_id', + }, + ] +] + +export const EXPECTED_OUTPUT_WITH_ONE_DEPTH = { + name: 'node1', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node1.id', + }, + children: [ + { + name: 'node2', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node2.id', + }, + children: [ + { + name: 'node4', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node4.id', + }, + children: [ + { + name: 'node7', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node7.id', + }, + children: [ + { + name: 'node10', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node10.id', + }, + children: [], + }, + ], + }, + ], + }, + ], + }, + { + name: 'node5', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node5.id', + }, + children: [], + }, + ], + }, + { + name: 'node6', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node6.id', + }, + children: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + schema: { + name: 'node11_id', + sql: 'node2.node11_id', + }, + children: [ + { + name: 'node11', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node11.id', + }, + children: [], + }, + ], + }, + ], + }, + ], + }, + { + name: 'node3', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node3.id', + }, + children: [ + { + name: 'node5', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node5.id', + }, + children: [], + }, + ], + }, + ], + }, + ], + }, + { + name: 'node6', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node6.id', + }, + children: [], + }, + ], + }, + ], + }, + ], +}; + +export const EXPECTED_OUTPUT_WITH_TWO_DEPTH = { + name: 'node1', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node1.id', + }, + children: [ + { + name: 'node2', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node2.id', + }, + children: [ + { + name: 'node4', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node4.id', + }, + children: [ + { + name: 'node7', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node7.id', + }, + children: [ + { + name: 'node10', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node10.id', + }, + children: [], + }, + ], + }, + ], + }, + ], + }, + { + name: 'node5', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node5.id', + }, + children: [ + { + name: 'node8', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node8.id', + }, + children: [], + }, + ], + }, + ], + }, + ], + }, + { + name: 'node6', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node6.id', + }, + children: [ + { + name: 'node9', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node9.id', + }, + children: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + schema: { + name: 'node11_id', + sql: 'node2.node11_id', + }, + children: [ + { + name: 'node11', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node11.id', + }, + children: [], + }, + ], + }, + ], + }, + ], + }, + { + name: 'node3', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node3.id', + }, + children: [ + { + name: 'node5', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node5.id', + }, + children: [ + { + name: 'node8', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node8.id', + }, + children: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + name: 'node6', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node6.id', + }, + children: [ + { + name: 'node9', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node9.id', + }, + children: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +}; + +export const CIRCULAR_TABLE_SCHEMA_SINGLE_JOIN_PATH = { + "name": "node1", + "measures": [], + "dimensions": [ + { + "schema": { + "name": "id", + "sql": "node1.id" + }, + "children": [ + { + "name": "node2", + "measures": [], + "dimensions": [ + { + "schema": { + "name": "id", + "sql": "node2.id" + }, + "children": [ + { + "name": "node3", + "measures": [], + "dimensions": [ + { + "schema": { + "name": "id", + "sql": "node3.id" + }, + "children": [] + } + ] + }, + { + "name": "node4", + "measures": [], + "dimensions": [ + { + "schema": { + "name": "id", + "sql": "node4.id" + }, + "children": [] + } + ] + } + ] + }, + { + "schema": { + "name": "node11_id", + "sql": "node2.node11_id" + }, + "children": [] + } + ] + }, + { + "name": "node3", + "measures": [], + "dimensions": [ + { + "schema": { + "name": "id", + "sql": "node3.id" + }, + "children": [ + { + "name": "node4", + "measures": [], + "dimensions": [ + { + "schema": { + "name": "id", + "sql": "node4.id" + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + } + ] +} + + +export const EXPECTED_CIRCULAR_TABLE_SCHEMA_INTERMEDIATE_JOIN_PATH = { + "name": "node1", + "measures": [], + "dimensions": [ + { + "schema": { + "name": "id", + "sql": "node1.id" + }, + "children": [ + { + "name": "node2", + "measures": [], + "dimensions": [ + { + "schema": { + "name": "id", + "sql": "node2.id" + }, + "children": [ + { + "name": "node4", + "measures": [], + "dimensions": [ + { + "schema": { + "name": "id", + "sql": "node4.id" + }, + "children": [] + } + ] + } + ] + }, + { + "schema": { + "name": "node11_id", + "sql": "node2.node11_id" + }, + "children": [] + } + ] + }, + { + "name": "node3", + "measures": [], + "dimensions": [ + { + "schema": { + "name": "id", + "sql": "node3.id" + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/meerkat-core/src/utils/get-possible-nodes.spec.ts b/meerkat-core/src/utils/get-possible-nodes.spec.ts index d0a8f17e..878c7cf8 100644 --- a/meerkat-core/src/utils/get-possible-nodes.spec.ts +++ b/meerkat-core/src/utils/get-possible-nodes.spec.ts @@ -1,1217 +1,645 @@ +import { BASIC_JOIN_PATH, CIRCULAR_JOIN_PATH, CIRCULAR_TABLE_SCHEMA, CIRCULAR_TABLE_SCHEMA_SINGLE_JOIN_PATH, COMPLEX_JOIN_PATH, EXPECTED_CIRCULAR_TABLE_SCHEMA_INTERMEDIATE_JOIN_PATH, EXPECTED_OUTPUT_WITH_ONE_DEPTH, EXPECTED_OUTPUT_WITH_TWO_DEPTH, INTERMEDIATE_JOIN_PATH, LINEAR_TABLE_SCHEMA, SINGLE_NODE_JOIN_PATH } from './__fixtures__/joins.fixtures'; import { getNestedTableSchema } from './get-possible-nodes'; describe('Table schema functions', () => { - const tableSchema = [ - { - name: 'node1', - dimensions: [ - { - name: 'id', - sql: 'node1.id', - }, - ], - measures: [], - sql: 'select * from node1', - joins: [ - { sql: 'node1.id = node2.id' }, - { sql: 'node1.id = node3.id' }, - { sql: 'node1.id = node6.id' }, - ], - }, - { - name: 'node2', - dimensions: [ - { - name: 'id', - sql: 'node2.id', - }, - { - name: 'node11_id', - sql: 'node2.node11_id', - }, - ], - measures: [], - sql: 'select * from node2', - joins: [ - { sql: 'node2.id = node4.id' }, - { sql: 'node2.node11_id = node11.id' }, - ], - }, - { - name: 'node3', - dimensions: [ - { - name: 'id', - sql: 'node3.id', - }, - ], - measures: [], - sql: 'select * from node3', - joins: [{ sql: 'node3.id = node5.id' }], - }, - { - name: 'node4', - dimensions: [ - { - name: 'id', - sql: 'node4.id', - }, - ], - measures: [], - sql: 'select * from node4', - joins: [ - { sql: 'node4.id = node5.id' }, - { sql: 'node4.id = node6.id' }, - { sql: 'node4.id = node7.id' }, - ], - }, - { - name: 'node5', - measures: [], - dimensions: [ - { - name: 'id', - sql: 'node5.id', - }, - ], - sql: 'select * from node5', - joins: [{ sql: 'node5.id = node8.id' }], - }, - { - name: 'node6', - dimensions: [ - { - name: 'id', - sql: 'node6.id', - }, - ], - measures: [], - sql: 'select * from node6', - joins: [{ sql: 'node6.id = node9.id' }], - }, - { - name: 'node7', - dimensions: [ - { - name: 'id', - sql: 'node7.id', - }, - ], - measures: [], - sql: 'select * from node7', - joins: [{ sql: 'node7.id = node10.id' }], - }, - { - name: 'node8', - dimensions: [ - { - name: 'id', - sql: 'node8.id', - }, - ], - measures: [], - sql: 'select * from node8', - joins: [], - }, - { - name: 'node9', - dimensions: [ - { - name: 'id', - sql: 'node9.id', - }, - ], - measures: [], - sql: 'select * from node9', - joins: [{ sql: 'node9.id = node10.id' }], - }, - { - name: 'node10', - dimensions: [ - { - name: 'id', - sql: 'node10.id', - }, - ], - measures: [], - sql: 'select * from node10', - joins: [], - }, - { - name: 'node11', - dimensions: [ - { - name: 'id', - sql: 'node11.id', - }, - ], - measures: [], - sql: 'select * from node11', - joins: [], - }, - ]; + describe('graph with no loops', () => { + it('Test single node join path', async () => { + const nestedSchema = await getNestedTableSchema( + LINEAR_TABLE_SCHEMA, + SINGLE_NODE_JOIN_PATH, + 1 + ); - const basicJoinPath = [ - [ - { - left: 'node1', - right: 'node2', - on: 'id', - }, - ], - ]; - - const singleNodeJoinPath = [ - [ - { - left: 'node1', - }, - ], - ]; - - const intermediateJoinPath = [ - [ - { - left: 'node1', - right: 'node2', - on: 'id', - }, - { - left: 'node2', - right: 'node4', - on: 'id', - }, - ], - [ - { - left: 'node1', - right: 'node3', - on: 'id', - }, - ], - ]; - - const complexJoinPath = [ - [ - { - left: 'node1', - right: 'node2', - on: 'id', - }, - { - left: 'node2', - right: 'node4', - on: 'id', - }, - { - left: 'node4', - right: 'node7', - on: 'id', - }, - ], - [ - { - left: 'node1', - right: 'node3', - on: 'id', - }, - ], - [ - { - left: 'node1', - right: 'node2', - on: 'id', - }, - { - left: 'node2', - right: 'node11', - on: 'node11_id', - }, - ], - ]; - - it('Test single node join path', async () => { - const nestedSchema = await getNestedTableSchema( - tableSchema, - singleNodeJoinPath, - 1 - ); - - expect(nestedSchema).toEqual({ - name: 'node1', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node1.id', - }, - children: [ - { - name: 'node2', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node2.id', - }, - children: [], - }, - { - schema: { - name: 'node11_id', - sql: 'node2.node11_id', - }, - children: [], - }, - ], + expect(nestedSchema).toEqual({ + name: 'node1', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node1.id', }, - { - name: 'node3', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node3.id', + children: [ + { + name: 'node2', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node2.id', + }, + children: [], }, - children: [], - }, - ], - }, - { - name: 'node6', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node6.id', + { + schema: { + name: 'node11_id', + sql: 'node2.node11_id', + }, + children: [], }, - children: [], - }, - ], - }, - ], - }, - ], - }); - }); - - it('Test basic join path with depth 0 (should return original graph)', async () => { - const nestedSchema = await getNestedTableSchema( - tableSchema, - basicJoinPath, - 0 - ); - - expect(nestedSchema).toEqual({ - name: 'node1', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node1.id', - }, - children: [ - { - name: 'node2', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node2.id', + ], + }, + { + name: 'node3', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node3.id', + }, + children: [], }, - children: [], - }, - { - schema: { - name: 'node11_id', - sql: 'node2.node11_id', + ], + }, + { + name: 'node6', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node6.id', + }, + children: [], }, - children: [], - }, - ], - }, - ], - }, - ], + ], + }, + ], + }, + ], + }); }); - }); - it('Test basic join path with depth 1', async () => { - const nestedSchema = await getNestedTableSchema( - tableSchema, - basicJoinPath, - 1 - ); + it('Test basic join path with depth 0 (should return original graph)', async () => { + const nestedSchema = await getNestedTableSchema( + LINEAR_TABLE_SCHEMA, + BASIC_JOIN_PATH, + 0 + ); - expect(nestedSchema).toEqual({ - name: 'node1', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node1.id', - }, - children: [ - { - name: 'node2', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node2.id', - }, - children: [ - { - name: 'node4', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node4.id', - }, - children: [], - }, - ], + expect(nestedSchema).toEqual({ + name: 'node1', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node1.id', + }, + children: [ + { + name: 'node2', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node2.id', }, - ], - }, - { - schema: { - name: 'node11_id', - sql: 'node2.node11_id', + children: [], }, - children: [ - { - name: 'node11', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node11.id', - }, - children: [], - }, - ], + { + schema: { + name: 'node11_id', + sql: 'node2.node11_id', }, - ], - }, - ], - }, - { - name: 'node3', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node3.id', + children: [], }, - children: [], - }, - ], - }, - { - name: 'node6', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node6.id', - }, - children: [], - }, - ], - }, - ], - }, - ], + ], + }, + ], + }, + ], + }); }); - }); - it('Test basic join path with depth 2', async () => { - const nestedSchema = await getNestedTableSchema( - tableSchema, - basicJoinPath, - 2 - ); + it('Test basic join path with depth 1', async () => { + const nestedSchema = await getNestedTableSchema( + LINEAR_TABLE_SCHEMA, + BASIC_JOIN_PATH, + 1 + ); - expect(nestedSchema).toEqual({ - name: 'node1', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node1.id', - }, - children: [ - { - name: 'node2', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node2.id', - }, - children: [ - { - name: 'node4', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node4.id', - }, - children: [ - { - name: 'node5', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node5.id', - }, - children: [], - }, - ], - }, - { - name: 'node6', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node6.id', - }, - children: [], - }, - ], - }, - { - name: 'node7', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node7.id', - }, - children: [], - }, - ], - }, - ], - }, - ], + expect(nestedSchema).toEqual({ + name: 'node1', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node1.id', + }, + children: [ + { + name: 'node2', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node2.id', }, - ], - }, - { - schema: { - name: 'node11_id', - sql: 'node2.node11_id', - }, - children: [ - { - name: 'node11', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node11.id', + children: [ + { + name: 'node4', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node4.id', + }, + children: [], }, - children: [], - }, - ], - }, - ], - }, - ], - }, - { - name: 'node3', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node3.id', + ], + }, + ], }, - children: [ - { - name: 'node5', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node5.id', - }, - children: [], - }, - ], + { + schema: { + name: 'node11_id', + sql: 'node2.node11_id', }, - ], - }, - ], - }, - { - name: 'node6', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node6.id', - }, - children: [ - { - name: 'node9', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node9.id', + children: [ + { + name: 'node11', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node11.id', + }, + children: [], }, - children: [], - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }); - }); - - it('Test intermediate join path with depth 0 (should return original graph)', async () => { - const nestedSchema = await getNestedTableSchema( - tableSchema, - intermediateJoinPath, - 0 - ); - - expect(nestedSchema).toEqual({ - name: 'node1', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node1.id', - }, - children: [ - { - name: 'node2', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node2.id', + ], + }, + ], }, - children: [ - { - name: 'node4', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node4.id', - }, - children: [], - }, - ], + ], + }, + { + name: 'node3', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node3.id', }, - ], - }, - { - schema: { - name: 'node11_id', - sql: 'node2.node11_id', + children: [], }, - children: [], - }, - ], - }, - { - name: 'node3', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node3.id', + ], + }, + { + name: 'node6', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node6.id', + }, + children: [], }, - children: [], - }, - ], - }, - ], - }, - ], + ], + }, + ], + }, + ], + }); }); - }); - it('Test intermediate join path with depth 1', async () => { - const nestedSchema = await getNestedTableSchema( - tableSchema, - intermediateJoinPath, - 1 - ); + it('Test basic join path with depth 2', async () => { + const nestedSchema = await getNestedTableSchema( + LINEAR_TABLE_SCHEMA, + BASIC_JOIN_PATH, + 2 + ); - expect(nestedSchema).toEqual({ - name: 'node1', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node1.id', - }, - children: [ - { - name: 'node2', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node2.id', - }, - children: [ - { - name: 'node4', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node4.id', - }, - children: [ - { - name: 'node5', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node5.id', - }, - children: [], - }, - ], + expect(nestedSchema).toEqual({ + name: 'node1', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node1.id', + }, + children: [ + { + name: 'node2', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node2.id', + }, + children: [ + { + name: 'node4', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node4.id', }, - { - name: 'node6', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node6.id', + children: [ + { + name: 'node5', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node5.id', + }, + children: [], }, - children: [], - }, - ], - }, - { - name: 'node7', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node7.id', + ], + }, + { + name: 'node6', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node6.id', + }, + children: [], }, - children: [], - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - schema: { - name: 'node11_id', - sql: 'node2.node11_id', - }, - children: [ - { - name: 'node11', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node11.id', + ], + }, + { + name: 'node7', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node7.id', + }, + children: [], + }, + ], + }, + ], }, - children: [], - }, - ], + ], + }, + ], + }, + { + schema: { + name: 'node11_id', + sql: 'node2.node11_id', }, - ], - }, - ], - }, - { - name: 'node3', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node3.id', + children: [ + { + name: 'node11', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node11.id', + }, + children: [], + }, + ], + }, + ], }, - children: [ - { - name: 'node5', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node5.id', + ], + }, + { + name: 'node3', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node3.id', + }, + children: [ + { + name: 'node5', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node5.id', + }, + children: [], }, - children: [], - }, - ], + ], + }, + ], + }, + ], + }, + { + name: 'node6', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node6.id', }, - ], - }, - ], - }, - { - name: 'node6', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node6.id', + children: [ + { + name: 'node9', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node9.id', + }, + children: [], + }, + ], + }, + ], }, - children: [], - }, - ], - }, - ], - }, - ], + ], + }, + ], + }, + ], + }); }); - }); - - it('Test complex join path with depth 1', async () => { - const nestedSchema = await getNestedTableSchema( - tableSchema, - complexJoinPath, - 1 - ); - - expect(nestedSchema).toEqual(expectedOutputWithOneDepth); - }); - it('Test complex complex join path with depth 2', async () => { - const nestedSchema = await getNestedTableSchema( - tableSchema, - complexJoinPath, - 2 - ); + it('Test intermediate join path with depth 0 (should return original graph)', async () => { + const nestedSchema = await getNestedTableSchema( + LINEAR_TABLE_SCHEMA, + INTERMEDIATE_JOIN_PATH, + 0 + ); - expect(nestedSchema).toEqual(expectedOutputWithTwoDepth); - }); -}); - -const expectedOutputWithOneDepth = { - name: 'node1', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node1.id', - }, - children: [ - { - name: 'node2', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node2.id', - }, - children: [ - { - name: 'node4', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node4.id', - }, - children: [ - { - name: 'node7', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node7.id', - }, - children: [ - { - name: 'node10', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node10.id', - }, - children: [], - }, - ], - }, - ], - }, - ], - }, - { - name: 'node5', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node5.id', - }, - children: [], - }, - ], - }, - { - name: 'node6', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node6.id', - }, - children: [], - }, - ], - }, - ], - }, - ], - }, - ], + expect(nestedSchema).toEqual({ + name: 'node1', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node1.id', }, - { - schema: { - name: 'node11_id', - sql: 'node2.node11_id', - }, - children: [ - { - name: 'node11', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node11.id', + children: [ + { + name: 'node2', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node2.id', + }, + children: [ + { + name: 'node4', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node4.id', + }, + children: [], + }, + ], }, - children: [], + ], + }, + { + schema: { + name: 'node11_id', + sql: 'node2.node11_id', }, - ], - }, - ], - }, - ], - }, - { - name: 'node3', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node3.id', + children: [], + }, + ], }, - children: [ - { - name: 'node5', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node5.id', - }, - children: [], + { + name: 'node3', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node3.id', }, - ], - }, - ], - }, - ], - }, - { - name: 'node6', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node6.id', + children: [], + }, + ], }, - children: [], - }, - ], - }, - ], - }, - ], -}; + ], + }, + ], + }); + }); -const expectedOutputWithTwoDepth = { - name: 'node1', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node1.id', - }, - children: [ - { - name: 'node2', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node2.id', - }, - children: [ - { - name: 'node4', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node4.id', - }, - children: [ - { - name: 'node7', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node7.id', - }, - children: [ - { - name: 'node10', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node10.id', - }, - children: [], - }, - ], - }, - ], + it('Test intermediate join path with depth 1', async () => { + const nestedSchema = await getNestedTableSchema( + LINEAR_TABLE_SCHEMA, + INTERMEDIATE_JOIN_PATH, + 1 + ); + + expect(nestedSchema).toEqual({ + name: 'node1', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node1.id', + }, + children: [ + { + name: 'node2', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node2.id', + }, + children: [ + { + name: 'node4', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node4.id', }, - ], - }, - { - name: 'node5', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node5.id', + children: [ + { + name: 'node5', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node5.id', + }, + children: [], + }, + ], }, - children: [ - { - name: 'node8', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node8.id', - }, - children: [], + { + name: 'node6', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node6.id', }, - ], - }, - ], - }, - ], - }, - { - name: 'node6', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node6.id', + children: [], + }, + ], }, - children: [ - { - name: 'node9', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node9.id', - }, - children: [], + { + name: 'node7', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node7.id', }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - schema: { - name: 'node11_id', - sql: 'node2.node11_id', - }, - children: [ - { - name: 'node11', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node11.id', + children: [], + }, + ], + }, + ], + }, + ], }, - children: [], + ], + }, + { + schema: { + name: 'node11_id', + sql: 'node2.node11_id', }, - ], - }, - ], - }, - ], - }, - { - name: 'node3', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node3.id', - }, - children: [ - { - name: 'node5', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node5.id', - }, - children: [ - { - name: 'node8', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node8.id', - }, - children: [], + children: [ + { + name: 'node11', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node11.id', }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - name: 'node6', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node6.id', + children: [], + }, + ], + }, + ], + }, + ], }, - children: [ - { - name: 'node9', - measures: [], - dimensions: [ - { - schema: { - name: 'id', - sql: 'node9.id', + { + name: 'node3', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node3.id', + }, + children: [ + { + name: 'node5', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node5.id', + }, + children: [], + }, + ], }, - children: [], + ], + }, + ], + }, + { + name: 'node6', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node6.id', }, - ], - }, - ], - }, - ], - }, - ], - }, - ], -}; + children: [], + }, + ], + }, + ], + }, + ], + }); + }); + + it('Test complex join path with depth 1', async () => { + const nestedSchema = await getNestedTableSchema( + LINEAR_TABLE_SCHEMA, + COMPLEX_JOIN_PATH, + 1 + ); + + expect(nestedSchema).toEqual(EXPECTED_OUTPUT_WITH_ONE_DEPTH); + }); + + it('Test complex complex join path with depth 2', async () => { + const nestedSchema = await getNestedTableSchema( + LINEAR_TABLE_SCHEMA, + COMPLEX_JOIN_PATH, + 2 + ); + + expect(nestedSchema).toEqual(EXPECTED_OUTPUT_WITH_TWO_DEPTH); + }); + }) + + describe('graph with loops', () => { + it('Test circular graphs', async () => { + const nestedSchema = getNestedTableSchema( + CIRCULAR_TABLE_SCHEMA, + SINGLE_NODE_JOIN_PATH, + 2 + ); + expect(nestedSchema).toEqual(CIRCULAR_TABLE_SCHEMA_SINGLE_JOIN_PATH); + }); + it('Test circular graphs with intermdiate path', async () => { + const nestedSchema = getNestedTableSchema( + CIRCULAR_TABLE_SCHEMA, + INTERMEDIATE_JOIN_PATH, + 2 + ); + expect(nestedSchema).toEqual(EXPECTED_CIRCULAR_TABLE_SCHEMA_INTERMEDIATE_JOIN_PATH); + }); + it('Should fail for circular selection path', () => { + expect(() => getNestedTableSchema(CIRCULAR_TABLE_SCHEMA, CIRCULAR_JOIN_PATH, 2)).toThrow('A loop was detected in the joins paths') + }); + }) +}); + + diff --git a/meerkat-core/src/utils/get-possible-nodes.ts b/meerkat-core/src/utils/get-possible-nodes.ts index ff8348a1..9fca3a18 100644 --- a/meerkat-core/src/utils/get-possible-nodes.ts +++ b/meerkat-core/src/utils/get-possible-nodes.ts @@ -1,4 +1,4 @@ -import { Graph, checkLoopInGraph, createDirectedGraph } from '../joins/joins'; +import { Graph, checkLoopInJoinPath, createDirectedGraph } from '../joins/joins'; import { Dimension, JoinPath, @@ -37,9 +37,10 @@ export const getNestedTableSchema = ( } const directedGraph = createDirectedGraph(tableSchemas, tableSchemaSqlMap); - const hasLoop = checkLoopInGraph(directedGraph); + + const hasLoop = checkLoopInJoinPath(joinPath); if (hasLoop) { - throw new Error('A loop was detected in the joins.'); + throw new Error('A loop was detected in the joins paths',); } const visitedNodes: { [key: string]: boolean } = {}; diff --git a/meerkat-node/package.json b/meerkat-node/package.json index 2a01d26f..0e68a9f1 100644 --- a/meerkat-node/package.json +++ b/meerkat-node/package.json @@ -1,6 +1,6 @@ { "name": "@devrev/meerkat-node", - "version": "0.0.72", + "version": "0.0.73", "dependencies": { "@swc/helpers": "~0.5.0", "@devrev/meerkat-core": "*", diff --git a/meerkat-node/src/__tests__/joins.spec.ts b/meerkat-node/src/__tests__/joins.spec.ts index 3b5a6780..cde8179b 100644 --- a/meerkat-node/src/__tests__/joins.spec.ts +++ b/meerkat-node/src/__tests__/joins.spec.ts @@ -301,7 +301,32 @@ describe('Joins Tests', () => { }; await expect( cubeQueryToSQL(query, [BOOK_SCHEMA, AUTHOR_SCHEMA]) - ).rejects.toThrow('A loop was detected in the joins.'); + ).rejects.toThrow('Invalid path, multiple data sources are present without a join path.'); + }); + + it('Loops in the join paths', async () => { + const query = { + measures: ['books.total_book_count'], + filters: [], + joinPaths: [ + [ + { + left: 'authors', + right: 'books', + on: 'author_id', + }, + { + left: 'books', + right: 'authors', + on: 'id', + }, + ], + ], + dimensions: ['authors.author_name'], + }; + await expect( + cubeQueryToSQL(query, [BOOK_SCHEMA, AUTHOR_SCHEMA]) + ).rejects.toThrow(`A loop was detected in the joins.`); }); it('Discrete Islands on data graph', async () => {