diff --git a/meerkat-core/package.json b/meerkat-core/package.json index cecff556..8e327c49 100644 --- a/meerkat-core/package.json +++ b/meerkat-core/package.json @@ -1,6 +1,6 @@ { "name": "@devrev/meerkat-core", - "version": "0.0.72", + "version": "0.0.73", "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 ff13ebf5..685be70e 100644 --- a/meerkat-core/src/joins/joins.spec.ts +++ b/meerkat-core/src/joins/joins.spec.ts @@ -87,7 +87,7 @@ describe('Table schema functions', () => { }); it('should correctly generate a SQL query from the provided join path, table schema SQL map, and directed graph', () => { - const joinPath = [ + const joinPaths = [ [ { left: 'table1', right: 'table2', on: 'id' }, { left: 'table2', right: 'table3', on: 'id' }, @@ -103,7 +103,7 @@ describe('Table schema functions', () => { table3: 'select * from table3', }; const sqlQuery = generateSqlQuery( - joinPath, + joinPaths, tableSchemaSqlMap, directedGraph ); diff --git a/meerkat-core/src/joins/joins.ts b/meerkat-core/src/joins/joins.ts index 83e5bd1f..30c4e0b9 100644 --- a/meerkat-core/src/joins/joins.ts +++ b/meerkat-core/src/joins/joins.ts @@ -1,11 +1,11 @@ -import { JoinEdge, Query, TableSchema } from '../types/cube-types'; +import { JoinPath, Query, TableSchema, isJoinNode } from '../types/cube-types'; export type Graph = { [key: string]: { [key: string]: { [key: string]: string } }; }; export function generateSqlQuery( - path: JoinEdge[][], + path: JoinPath[], tableSchemaSqlMap: { [key: string]: string }, directedGraph: Graph ): string { @@ -18,6 +18,14 @@ export function generateSqlQuery( const startingNode = path[0][0].left; let query = `${tableSchemaSqlMap[startingNode]}`; + /** + * If the starting node is not a join node, then return the query as is. + * It means that the query is a single node query. + */ + if (!isJoinNode(path[0][0])) { + return query; + } + const visitedNodes = new Map(); for (let i = 0; i < path.length; i++) { @@ -28,6 +36,11 @@ export function generateSqlQuery( } for (let j = 0; j < path[i].length; j++) { const currentEdge = path[i][j]; + + if (!isJoinNode(currentEdge)) { + continue; + } + const visitedFrom = visitedNodes.get(currentEdge.right); // If node is already visited from the same edge, continue to next iteration @@ -194,10 +207,8 @@ export const getCombinedTableSchema = async ( throw new Error('A loop was detected in the joins.'); } - console.log('directedGraph', directedGraph); - const baseSql = generateSqlQuery( - cubeQuery.joinPath || [], + cubeQuery.joinPaths || [], tableSchemaSqlMap, directedGraph ); diff --git a/meerkat-core/src/types/cube-types/query.ts b/meerkat-core/src/types/cube-types/query.ts index f4936dc3..54043150 100644 --- a/meerkat-core/src/types/cube-types/query.ts +++ b/meerkat-core/src/types/cube-types/query.ts @@ -115,7 +115,7 @@ interface QueryTimeDimension { * Join Edge data type. */ -interface JoinEdge { +interface JoinNode { /** * Left node. */ @@ -152,6 +152,23 @@ interface JoinEdge { */ } +/** + * Single node data type. + * This is the case when there is no join. Just a single node. + */ +interface SingleNode { + /** + * Left node. + */ + left: Member; +} + +type JoinPath = [JoinNode | SingleNode, ...JoinNode[]]; + +export const isJoinNode = (node: JoinNode | SingleNode): node is JoinNode => { + return 'right' in node; +}; + /** * Incoming network query data type. */ @@ -163,7 +180,7 @@ interface Query { dimensions?: (Member | TimeMember)[]; filters?: MeerkatQueryFilter[]; timeDimensions?: QueryTimeDimension[]; - joinPath?: JoinEdge[][]; + joinPaths?: JoinPath[]; segments?: Member[]; limit?: null | number; offset?: number; @@ -196,7 +213,7 @@ export { ApiScopes, ApiType, FilterOperator, - JoinEdge, + JoinPath, LogicalAndFilter, LogicalOrFilter, MeerkatQueryFilter, diff --git a/meerkat-core/src/utils/get-possible-nodes.spec.ts b/meerkat-core/src/utils/get-possible-nodes.spec.ts index 7a62a5fd..d0a8f17e 100644 --- a/meerkat-core/src/utils/get-possible-nodes.spec.ts +++ b/meerkat-core/src/utils/get-possible-nodes.spec.ts @@ -161,6 +161,14 @@ describe('Table schema functions', () => { ], ]; + const singleNodeJoinPath = [ + [ + { + left: 'node1', + }, + ], + ]; + const intermediateJoinPath = [ [ { @@ -222,6 +230,75 @@ describe('Table schema functions', () => { ], ]; + 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: [], + }, + ], + }, + { + name: 'node3', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node3.id', + }, + children: [], + }, + ], + }, + { + name: 'node6', + measures: [], + dimensions: [ + { + schema: { + name: 'id', + sql: 'node6.id', + }, + children: [], + }, + ], + }, + ], + }, + ], + }); + }); + it('Test basic join path with depth 0 (should return original graph)', async () => { const nestedSchema = await getNestedTableSchema( tableSchema, diff --git a/meerkat-core/src/utils/get-possible-nodes.ts b/meerkat-core/src/utils/get-possible-nodes.ts index 65611dab..ff8348a1 100644 --- a/meerkat-core/src/utils/get-possible-nodes.ts +++ b/meerkat-core/src/utils/get-possible-nodes.ts @@ -1,5 +1,11 @@ import { Graph, checkLoopInGraph, createDirectedGraph } from '../joins/joins'; -import { Dimension, JoinEdge, Measure, TableSchema } from '../types/cube-types'; +import { + Dimension, + JoinPath, + Measure, + TableSchema, + isJoinNode, +} from '../types/cube-types'; export interface NestedMeasure { schema: Measure; @@ -19,7 +25,7 @@ export interface NestedTableSchema { export const getNestedTableSchema = ( tableSchemas: TableSchema[], - joinPath: JoinEdge[][], + joinPath: JoinPath[], depth: number ) => { const tableSchemaSqlMap: { [key: string]: string } = {}; @@ -58,12 +64,22 @@ export const getNestedTableSchema = ( const checkedPaths: { [key: string]: boolean } = {}; const buildNestedSchema = ( - edges: JoinEdge[], + edges: JoinPath, index: number, nestedTableSchema: NestedTableSchema, tableSchemas: TableSchema[] ): NestedTableSchema => { const edge = edges[index]; + + /** + * If there is no right table, return the nested schema immediately + * This means there is a single node in the path. + */ + + if (!isJoinNode(edge)) { + return nestedTableSchema; + } + // If the path has been checked before, return the nested schema immediately const pathKey = `${edge.left}-${edge.right}-${edge.on}`; if (checkedPaths[pathKey]) { @@ -82,6 +98,10 @@ export const getNestedTableSchema = ( (schema) => schema.name === edge.right ) as TableSchema; + if (!rightSchema) { + throw new Error(`The schema for ${edge.right} does not exist.`); + } + // Mark the path as checked checkedPaths[pathKey] = true; diff --git a/meerkat-node/src/__tests__/joins.spec.ts b/meerkat-node/src/__tests__/joins.spec.ts index 16f1ff38..3b5a6780 100644 --- a/meerkat-node/src/__tests__/joins.spec.ts +++ b/meerkat-node/src/__tests__/joins.spec.ts @@ -308,7 +308,7 @@ describe('Joins Tests', () => { BOOK_SCHEMA.joins = []; const query = { measures: ['books.total_book_count', 'authors.total_author_count'], - joinPath: [ + joinPaths: [ [ { left: 'authors', @@ -340,6 +340,31 @@ describe('Joins Tests', () => { ); }); + it('Single node in the path', async () => { + const query = { + measures: [], + filters: [], + dimensions: ['orders.order_id'], + joinPaths: [ + [ + { + left: 'orders', + on: 'order_id', + }, + ], + ], + }; + const sql = await cubeQueryToSQL(query, [AUTHOR_SCHEMA, ORDER_SCHEMA]); + console.info(`SQL for Simple Cube Query: `, sql); + const output = await duckdbExec(sql); + const parsedOutput = JSON.parse(JSON.stringify(output)); + console.info('parsedOutput', parsedOutput); + expect(sql).toEqual( + 'SELECT orders__order_id FROM (SELECT *, orders.order_id AS orders__order_id FROM (select * from orders) AS orders) AS MEERKAT_GENERATED_TABLE GROUP BY orders__order_id' + ); + expect(parsedOutput).toHaveLength(11); + }); + it('Three tables join - Direct', async () => { const DEMO_SCHEMA = structuredClone(ORDER_SCHEMA); @@ -351,7 +376,7 @@ describe('Joins Tests', () => { const query = { measures: ['orders.total_order_amount'], - joinPath: [ + joinPaths: [ [ { left: 'customers', @@ -408,7 +433,7 @@ describe('Joins Tests', () => { const query = { measures: ['orders.total_order_amount'], - joinPath: [ + joinPaths: [ [ { left: 'customers', @@ -465,7 +490,7 @@ describe('Joins Tests', () => { it('Joins with Different Paths', async () => { const query1 = { measures: ['orders.total_order_amount'], - joinPath: [ + joinPaths: [ [ { left: 'customers', @@ -512,7 +537,7 @@ describe('Joins Tests', () => { const query2 = { measures: ['orders.total_order_amount'], - joinPath: [ + joinPaths: [ [ { left: 'customers', @@ -555,7 +580,7 @@ describe('Joins Tests', () => { it('Success Join with filters', async () => { const query = { measures: ['orders.total_order_amount'], - joinPath: [ + joinPaths: [ [ { left: 'customers',