diff --git a/src/queryExecution/cypher/converter/logic.ts b/src/queryExecution/cypher/converter/logic.ts index 4ef8a20d99ef05f57ee9f73d7259d90a3eeaf4c0..301053945275c66e4310e2cc2f53ca0fb44cd1e1 100644 --- a/src/queryExecution/cypher/converter/logic.ts +++ b/src/queryExecution/cypher/converter/logic.ts @@ -27,12 +27,16 @@ export function createWhereLogic( return { logic: newWhereLogic, where: whereLogic }; } -export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCacheData): { logic: string; where: Set<string> } { +export function extractLogicCypher( + logicQuery: AnyStatement | AnyStatement[], + cacheData: QueryCacheData, +): { logic: string; where: Set<string> } { switch (typeof logicQuery) { case 'object': if (Array.isArray(logicQuery)) { - let op = logicQuery[0].replace('_', '').toLowerCase(); - let right = logicQuery?.[2]; + logicQuery = logicQuery as AnyStatement[]; + let op = (logicQuery[0] as string).replace('_', '').toLowerCase(); + const right = logicQuery?.[2]; const { logic: left, where: whereLogic } = extractLogicCypher(logicQuery[1], cacheData); switch (op) { diff --git a/src/queryExecution/sql/convertLogicToSQL.ts b/src/queryExecution/sql/convertLogicToSQL.ts new file mode 100644 index 0000000000000000000000000000000000000000..055910f0970041fe04b773fbfe6db263632faa22 --- /dev/null +++ b/src/queryExecution/sql/convertLogicToSQL.ts @@ -0,0 +1,158 @@ +import { StringFilterTypes, type AnyStatement } from 'ts-common/src/model/query/logic/general'; +import { log } from 'ts-common/src/logger/logger'; +import type { NodeStruct } from 'ts-common'; + +export type SQLCacheData = { + nodes: NodeStruct[]; +}; + +export function createWhereLogic( + op: string, + left: string, + whereLogic: Set<string>, + cacheData: SQLCacheData, +): { logic: string; where: Set<string> } { + const newWhereLogic = `${left.replace('.', '_')}_${op}`; + const tableName = left.split('.')[0]; + + whereLogic.add(`${op}(${left}) AS ${newWhereLogic}`); + for (const entity of cacheData.nodes) { + if (entity.id !== tableName && entity.id) { + whereLogic.add(entity.id); + } + } + + return { logic: newWhereLogic, where: whereLogic }; +} + +export function extractLogicSQL(logicQuery: AnyStatement | AnyStatement[], cacheData: SQLCacheData): { logic: string; where: Set<string> } { + switch (typeof logicQuery) { + case 'object': + if (Array.isArray(logicQuery)) { + logicQuery = logicQuery as AnyStatement[]; + let op = (logicQuery[0] as string).replace('_', '').toLowerCase(); + const right = logicQuery?.[2]; + const { logic: left, where: whereLogic } = extractLogicSQL(logicQuery[1], cacheData); + + if (left === '') { + return { logic: '', where: whereLogic }; + } + + switch (op) { + case '!=': + op = '!='; + break; + case '==': + op = '='; + break; + case 'like': + return { + logic: `${left} LIKE '%${(right as string).replace(/['"]/g, '')}%'`, + where: whereLogic, + }; + case 'in': + case 'present in list': + if (typeof right === 'string') { + return { + logic: `${left} IN (${right + .replace(/"/g, "'") + .split(',') + .map((r: string) => r.trim()) + .join(', ')})`, + where: whereLogic, + }; + } + break; + case 'not in': + case 'not in list': + if (typeof right === 'string') { + return { + logic: `${left} NOT IN (${right + .replace(/"/g, "'") + .split(',') + .map((r: string) => r.trim()) + .join(', ')})`, + where: whereLogic, + }; + } + break; + case 'isempty': + return { logic: `(${left} IS NULL OR ${left} = '')`, where: whereLogic }; + case 'isnotempty': + return { logic: `(${left} IS NOT NULL AND ${left} <> '')`, where: whereLogic }; + case 'lower': + return { logic: `LOWER(${left})`, where: whereLogic }; + case 'upper': + return { logic: `UPPER(${left})`, where: whereLogic }; + case 'avg': + return { + logic: `(SELECT AVG(${left}) FROM ${left.split('.')[0]})`, + where: whereLogic, + }; + case 'count': + return { + logic: `(SELECT COUNT(${left}) FROM ${left.split('.')[0]})`, + where: whereLogic, + }; + case 'max': + return { + logic: `(SELECT MAX(${left}) FROM ${left.split('.')[0]})`, + where: whereLogic, + }; + case 'min': + return { + logic: `(SELECT MIN(${left}) FROM ${left.split('.')[0]})`, + where: whereLogic, + }; + case 'sum': + return { + logic: `(SELECT SUM(${left}) FROM ${left.split('.')[0]})`, + where: whereLogic, + }; + case 'and': + if (logicQuery.length > 2) { + const conditions = logicQuery.slice(1).map(condition => { + const { logic: condLogic } = extractLogicSQL(condition, cacheData); + return condLogic; + }); + return { logic: `(${conditions.join(' AND ')})`, where: whereLogic }; + } + break; + case 'or': + if (logicQuery.length > 2) { + const conditions = logicQuery.slice(1).map(condition => { + const { logic: condLogic } = extractLogicSQL(condition, cacheData); + return condLogic; + }); + return { logic: `(${conditions.join(' OR ')})`, where: whereLogic }; + } + break; + } + if (logicQuery.length > 2 && logicQuery[2] !== undefined) { + const { logic: rightLogic, where: whereLogicRight } = extractLogicSQL(logicQuery[2], cacheData); + + for (const where of whereLogicRight) { + whereLogic.add(where); + } + + return { logic: `(${left} ${op} ${rightLogic})`, where: whereLogic }; + } + return { logic: `(${op} ${left})`, where: whereLogic }; + } + return { logic: logicQuery, where: new Set() }; + case 'string': + // Handle string literals and field references + if (logicQuery.startsWith('"') && logicQuery.endsWith('"')) { + return { logic: logicQuery.replace(/"/g, "'"), where: new Set() }; + } + if (logicQuery.startsWith('@')) { + return { logic: logicQuery.replace('@', ''), where: new Set() }; + } else { + return { logic: `'${logicQuery}'`, where: new Set() }; + } + case 'number': + return { logic: logicQuery.toString(), where: new Set() }; + default: + return { logic: logicQuery as any, where: new Set() }; + } +} diff --git a/src/queryExecution/sql/queryConverterSql copy.ts b/src/queryExecution/sql/queryConverterSql copy.ts new file mode 100644 index 0000000000000000000000000000000000000000..c370a1e5176fe5a70210af84df4f8b1a562abe6d --- /dev/null +++ b/src/queryExecution/sql/queryConverterSql copy.ts @@ -0,0 +1,158 @@ +import type { BackendQueryFormat, NodeStruct } from 'ts-common'; +import type { QueryText } from '../model'; + +type Logic = any; + +function cleanStringOfNewLinesAndTabs(str: string): string { + if (!str) { + return ''; + } + let trimmedSQL = str.replace(/\n/g, ' '); + trimmedSQL = trimmedSQL.replaceAll(/ {2,50}/g, ' '); + trimmedSQL = trimmedSQL.replace(/\t+/g, ''); + return trimmedSQL.trim(); +} + +function convertLogicToSQL(logic: Logic, nodeAlias: string): string { + if (!logic || !Array.isArray(logic)) return ''; + + const operator = logic[0]; + const left = logic[1]; + const right = logic[2]; + + // Handle field references (e.g., @p1.name) + if (typeof left === 'string' && left.startsWith('@')) { + const [alias, field] = left.substring(1).split('.'); + if (alias === nodeAlias) { + return `${alias}.${field} ${operator} '${right.replace(/^"(.*)"$/, '$1')}'`; + } + } + return ''; +} + +function buildNodeCTE(node: NodeStruct | undefined, logic?: Logic, parent: NodeStruct | undefined = undefined): string[] { + if (!node || !node.label || !node.id) { + return []; + } + const nodeParts: string[] = []; + const logicClause = convertLogicToSQL(logic, node.id); + const whereClause = logic && logicClause ? `WHERE ${convertLogicToSQL(logic, node.id)}` : ''; + const mainNode = ` + SELECT jsonb_build_object( + '_id', concat('${node.label}-', ${node.id}.id::text), + 'label', '${node.label}', + 'attributes', to_jsonb(${node.id}) - 'id' + ) AS node + FROM ${node.label} ${node.id} + ${ + parent && parent.label && parent.id && parent.relation && parent.relation.label + ? `INNER JOIN ${parent.label} ${parent.id} ON ${parent.id}.${parent.relation.label.split('.')[0]} = ${node.id}.${ + parent.relation.label.split('.')[1] + }` + : '' + } + ${whereClause}`; + nodeParts.push(cleanStringOfNewLinesAndTabs(mainNode)); + buildNodeCTE(node.relation?.node, logic, node).forEach(part => nodeParts.push(part)); + return nodeParts; +} + +function buildEdgesCTE(node: NodeStruct | undefined, logic?: Logic, parent: NodeStruct | undefined = undefined): string[] { + if (!node || !node.relation || !node.relation.node || !node.id || !node.relation.node.id || !node.relation.label) { + return []; + } + const logicClause = convertLogicToSQL(logic, node.id); + const whereClause = logic && logicClause ? `WHERE ${convertLogicToSQL(logic, node.id)}` : ''; + + const edge = ` + SELECT jsonb_agg( + jsonb_build_object( + '_id', concat('${node.label}-', ${node.id}.id::text, '-${node.relation.node.label}-', ${node.relation.node.id}.id::text), + 'label', '${node.relation.label}', + 'attributes', '{}'::jsonb, + 'from', ${node.id}.id::text, + 'to', ${node.relation.node.id}.id::text + ) + ) AS edge + FROM ${node.label} ${node.id} + INNER JOIN ${node.relation.node.label} ${node.relation.node.id} ON ${node.id}.${node.relation.label.split('.')[0]} = ${ + node.relation.node.id + }.${node.relation.label.split('.')[1]} + ${whereClause}`; + const ret = cleanStringOfNewLinesAndTabs(edge); + return [ret, ...buildEdgesCTE(node.relation.node, logic, node)]; +} + +function buildNodeCountsCTE(node: NodeStruct | undefined): string[] { + if (!node || !node.label || !node.id) { + return []; + } + const nodeParts: string[] = []; + const mainNode = `SELECT '${node.label}' AS label FROM ${node.label}`; + nodeParts.push(cleanStringOfNewLinesAndTabs(mainNode)); + buildNodeCountsCTE(node.relation?.node).forEach(part => nodeParts.push(part)); + return nodeParts; +} + +function buildEdgeCountsCTE(node: NodeStruct | undefined): string[] { + if (!node || !node.relation || !node.relation.node || !node.id || !node.relation.node.id || !node.relation.label) { + return []; + } + + const edge = ` + SELECT '${node.relation.label}' AS label, COUNT(*) AS count + FROM ${node.label} ${node.id} + JOIN ${node.relation.node.label} ${node.relation.node.id} + ON ${node.id}.${node.relation.label.split('.')[0]} = ${node.relation.node.id}.${node.relation.label.split('.')[1]} + GROUP BY label`; + const ret = cleanStringOfNewLinesAndTabs(edge); + return [ret, ...buildEdgeCountsCTE(node.relation.node)]; +} + +export function query2SQL(query: BackendQueryFormat): QueryText { + const nodesCTE = [...new Set(query.query.flatMap(path => buildNodeCTE(path.node, query.logic)))]; + const edgesCTE = [...new Set(query.query.flatMap(path => buildEdgesCTE(path.node)))]; + const nodeCountsCTE = [...new Set(query.query.flatMap(path => buildNodeCountsCTE(path.node)))]; + const edgeCountsCTE = [...new Set(query.query.flatMap(path => buildEdgeCountsCTE(path.node)))]; + + const mainQuery = `WITH raw_nodes AS ( + SELECT node + FROM ( + ${nodesCTE.join(' UNION ALL ')} + ) sub + ${query.limit ? `LIMIT ${query.limit}` : ''} + ), + nodes_cte AS ( + SELECT jsonb_agg(node) AS nodes + FROM raw_nodes + ), + edges_cte AS ( + SELECT jsonb_agg(edge) AS edges + FROM ( + ${edgesCTE.join(' UNION ALL ')} + ) aggregated_edges + ), + node_counts AS ( + SELECT label, COUNT(*) AS count + FROM ( + ${nodeCountsCTE.join(' UNION ALL ')} + ) t + GROUP BY label + ), + edge_counts AS ( + ${edgeCountsCTE.join(' UNION ALL ')} + ) + SELECT jsonb_build_object( + 'nodes', nodes_cte.nodes, + 'edges', COALESCE(edges_cte.edges, '[]'::jsonb), + 'nodeCounts', COALESCE( (SELECT jsonb_agg(jsonb_build_object('label', label, 'count', count)) + FROM node_counts), '[]'::jsonb ), + 'edgeCounts', COALESCE( (SELECT jsonb_agg(jsonb_build_object('label', label, 'count', count)) + FROM edge_counts), '[]'::jsonb ) + ) AS result FROM nodes_cte, edges_cte;`; + + return { + query: mainQuery, + countQuery: '', + }; +} diff --git a/src/queryExecution/sql/queryConverterSql.test.ts b/src/queryExecution/sql/queryConverterSql.test.ts index 3773a70957c6b45c29ea190dd489fecea65a8964..7de4d9f0710756c30c48a5c3ed239bdd4742fc16 100644 --- a/src/queryExecution/sql/queryConverterSql.test.ts +++ b/src/queryExecution/sql/queryConverterSql.test.ts @@ -166,7 +166,7 @@ FROM nodes_cte, edges_cte;`; const query: BackendQueryFormat = { saveStateID: 'test', return: ['*'], - logic: ['==', '@uu.username', '"Username1"'], + logic: ['!=', '@uu.username', '"Username1"'], query: [ { id: 'path1', diff --git a/src/queryExecution/sql/queryConverterSql.ts b/src/queryExecution/sql/queryConverterSql.ts index c370a1e5176fe5a70210af84df4f8b1a562abe6d..6d91a1d313aa97f1a953666bf9986b130789c247 100644 --- a/src/queryExecution/sql/queryConverterSql.ts +++ b/src/queryExecution/sql/queryConverterSql.ts @@ -1,7 +1,7 @@ -import type { BackendQueryFormat, NodeStruct } from 'ts-common'; +import type { AllLogicStatement, AnyStatement, BackendQueryFormat, NodeStruct, QueryStruct } from 'ts-common'; import type { QueryText } from '../model'; - -type Logic = any; +import { extractLogicSQL } from './convertLogicToSQL'; +import type { QueryCacheData } from '../cypher/converter/model'; function cleanStringOfNewLinesAndTabs(str: string): string { if (!str) { @@ -13,146 +13,272 @@ function cleanStringOfNewLinesAndTabs(str: string): string { return trimmedSQL.trim(); } -function convertLogicToSQL(logic: Logic, nodeAlias: string): string { - if (!logic || !Array.isArray(logic)) return ''; +// function buildNodeCTE(path: QueryStruct, logic: AnyStatement | undefined): { node: string; edges: string; rows: string[] } { +// const node = path.node; +// const pathId = path.id; + +// if (!node || !node.label || !node.id) { +// return { node: '', edges: '', rows: [] }; +// } + +// const loopThoughNodes = (node: NodeStruct): NodeStruct[] => { +// if (node.relation && node.relation.node) { +// return [node, ...loopThoughNodes(node.relation.node)]; +// } +// return [node]; +// }; +// const allNodes = loopThoughNodes(node); + +// const where = logic ? extractLogicSQL(logic, { nodes: allNodes }).logic : undefined; +// const rowList = allNodes.map(n => `${n.id}_row`); +// const select = ` +// SELECT ${[ +// ...allNodes.map(n => `to_jsonb(${n.id}) AS ${n.id}_row`), +// ...allNodes +// .filter(n => n.relation && n!.relation!.node) +// .map( +// n => +// `${n.id}.${n!.relation!.label!.split('.')[0]}::text AS ${n.id}_${n!.relation!.label!.split('.')[0]}, ${n!.relation!.node!.id}.${ +// n!.relation!.label!.split('.')[1] +// }::text AS ${n!.relation!.node!.id}_${n!.relation!.label!.split('.')[1]}`, +// ), +// ].join(', ')} +// FROM ${node.label} ${node.id} +// ${allNodes +// .filter(n => n?.relation?.node?.label && n?.relation?.node?.id) +// .map( +// n => +// `INNER JOIN ${n!.relation!.node!.label} ${n!.relation!.node!.id} ON ${n.id}.${n!.relation!.label!.split('.')[0]}::text = ${ +// n!.relation!.node!.id +// }.${n!.relation!.label!.split('.')[1]}::text`, +// ) +// .join('\n')} +// ${where ? `WHERE ${where}` : ''}`; + +// const edges = buildEdgesCTE(node, rowList, pathId).join(' UNION ALL '); + +// return { node: select, edges, rows: rowList }; +// } + +// function buildEdgesCTE(node: NodeStruct | undefined, rowList: string[], pathId: string): string[] { +// if (!node || !node.relation || !node.relation.node || !node.id || !node.relation.node.id || !node.relation.label) { +// return []; +// } + +// const edge = ` +// SELECT jsonb_build_object( +// '_id', concat('${node.label}-', ${node.id}_${node.relation.label.split('.')[0]}::text, '-${node.relation.node.label}-', ${ +// node.relation.node.id +// }_${node.relation.label.split('.')[1]}::text), +// 'label', '${node.relation.label}', +// 'attributes', '{}'::jsonb, +// 'from', ${node.id}_${node.relation.label.split('.')[0]}::text, +// 'to', ${node.relation.node.id}_${node.relation.label.split('.')[1]}::text +// ) AS edge, +// ${rowList.join(',\n')} +// FROM ${pathId}`; + +// const ret = cleanStringOfNewLinesAndTabs(edge); +// return [ret, ...buildEdgesCTE(node.relation.node, rowList, pathId)]; +// } + +// function buildNodeCountsCTE(node: NodeStruct | undefined): string[] { +// if (!node || !node.label || !node.id) { +// return []; +// } +// const nodeParts: string[] = []; +// const mainNode = `SELECT '${node.label}' AS label FROM ${node.label}`; +// nodeParts.push(cleanStringOfNewLinesAndTabs(mainNode)); +// buildNodeCountsCTE(node.relation?.node).forEach(part => nodeParts.push(part)); +// return nodeParts; +// } - const operator = logic[0]; - const left = logic[1]; - const right = logic[2]; +// function buildEdgeCountsCTE(node: NodeStruct | undefined): string[] { +// if (!node || !node.relation || !node.relation.node || !node.id || !node.relation.node.id || !node.relation.label) { +// return []; +// } - // Handle field references (e.g., @p1.name) - if (typeof left === 'string' && left.startsWith('@')) { - const [alias, field] = left.substring(1).split('.'); - if (alias === nodeAlias) { - return `${alias}.${field} ${operator} '${right.replace(/^"(.*)"$/, '$1')}'`; +// const edge = ` +// SELECT '${node.relation.label}' AS label, COUNT(*) AS count +// FROM ${node.label} ${node.id} +// JOIN ${node.relation.node.label} ${node.relation.node.id} +// ON ${node.id}.${node.relation.label.split('.')[0]} = ${node.relation.node.id}.${node.relation.label.split('.')[1]} +// GROUP BY label`; +// const ret = cleanStringOfNewLinesAndTabs(edge); +// return [ret, ...buildEdgeCountsCTE(node.relation.node)]; +// } + +export function query2SQL(query: BackendQueryFormat): QueryText { + const loopThoughNodes = (node: NodeStruct): NodeStruct[] => { + if (node.relation && node.relation.node) { + return [node, ...loopThoughNodes(node.relation.node)]; } - } - return ''; -} + return [node]; + }; + const allNodes = query.query.flatMap(path => loopThoughNodes(path.node)); + const allUniqueNodes: Record< + string, + { label: string; joins: { from: string; leftJoin: string; rightJoin: string; to: string; fromLabel: string; toLabel: string }[] } + > = Object.fromEntries( + allNodes + .map(n => [n.id, { label: n.label, joins: [] }]) + .filter((node, index, self) => { + return index === self.findIndex(t => t[0] === node[0] && t[1] === node[1]); + }), + ); + allNodes.forEach(n => { + if (n.id && n.relation && n.relation.node && n.relation.node.id && n.label && n.relation.node.label) { + allUniqueNodes[n.id].joins.push({ + from: n.id, + leftJoin: n!.relation!.label!.split('.')[0], + rightJoin: n!.relation!.label!.split('.')[1], + to: n!.relation!.node!.id, + fromLabel: n!.label, + toLabel: n!.relation!.node!.label, + }); + } + }); -function buildNodeCTE(node: NodeStruct | undefined, logic?: Logic, parent: NodeStruct | undefined = undefined): string[] { - if (!node || !node.label || !node.id) { - return []; - } - const nodeParts: string[] = []; - const logicClause = convertLogicToSQL(logic, node.id); - const whereClause = logic && logicClause ? `WHERE ${convertLogicToSQL(logic, node.id)}` : ''; - const mainNode = ` - SELECT jsonb_build_object( - '_id', concat('${node.label}-', ${node.id}.id::text), - 'label', '${node.label}', - 'attributes', to_jsonb(${node.id}) - 'id' - ) AS node - FROM ${node.label} ${node.id} - ${ - parent && parent.label && parent.id && parent.relation && parent.relation.label - ? `INNER JOIN ${parent.label} ${parent.id} ON ${parent.id}.${parent.relation.label.split('.')[0]} = ${node.id}.${ - parent.relation.label.split('.')[1] - }` - : '' - } - ${whereClause}`; - nodeParts.push(cleanStringOfNewLinesAndTabs(mainNode)); - buildNodeCTE(node.relation?.node, logic, node).forEach(part => nodeParts.push(part)); - return nodeParts; -} + console.log(JSON.stringify(allNodes, null, 2)); -function buildEdgesCTE(node: NodeStruct | undefined, logic?: Logic, parent: NodeStruct | undefined = undefined): string[] { - if (!node || !node.relation || !node.relation.node || !node.id || !node.relation.node.id || !node.relation.label) { - return []; - } - const logicClause = convertLogicToSQL(logic, node.id); - const whereClause = logic && logicClause ? `WHERE ${convertLogicToSQL(logic, node.id)}` : ''; - - const edge = ` - SELECT jsonb_agg( - jsonb_build_object( - '_id', concat('${node.label}-', ${node.id}.id::text, '-${node.relation.node.label}-', ${node.relation.node.id}.id::text), - 'label', '${node.relation.label}', - 'attributes', '{}'::jsonb, - 'from', ${node.id}.id::text, - 'to', ${node.relation.node.id}.id::text + const nodeEntries = Object.entries(allUniqueNodes); + const where = query.logic ? extractLogicSQL(query.logic, { nodes: allNodes }).logic : undefined; + + const firstNode = nodeEntries[0][1]; + + const relIDSql = Object.values(allUniqueNodes) + .map(n => + n.joins.map(j => `${j.from}.${j.leftJoin} AS ${j.from}_${j.leftJoin}, ${j.to}.${j.rightJoin} AS ${j.to}_${j.rightJoin}`).join(', '), ) - ) AS edge - FROM ${node.label} ${node.id} - INNER JOIN ${node.relation.node.label} ${node.relation.node.id} ON ${node.id}.${node.relation.label.split('.')[0]} = ${ - node.relation.node.id - }.${node.relation.label.split('.')[1]} - ${whereClause}`; - const ret = cleanStringOfNewLinesAndTabs(edge); - return [ret, ...buildEdgesCTE(node.relation.node, logic, node)]; -} + .filter(s => s.length > 0); + const select = [...nodeEntries.map(n => `to_jsonb(${n[0]}) AS ${n[0]}_row`), ...relIDSql].join(', '); -function buildNodeCountsCTE(node: NodeStruct | undefined): string[] { - if (!node || !node.label || !node.id) { - return []; - } - const nodeParts: string[] = []; - const mainNode = `SELECT '${node.label}' AS label FROM ${node.label}`; - nodeParts.push(cleanStringOfNewLinesAndTabs(mainNode)); - buildNodeCountsCTE(node.relation?.node).forEach(part => nodeParts.push(part)); - return nodeParts; -} + let sql = `SELECT ${select} FROM ${firstNode.label} ${nodeEntries[0][0]}`; + // let sql = `SELECT jsonb_agg(row_to_json(t)) FROM ${firstNode.label} ${nodeEntries[0][0]}`; -function buildEdgeCountsCTE(node: NodeStruct | undefined): string[] { - if (!node || !node.relation || !node.relation.node || !node.id || !node.relation.node.id || !node.relation.label) { - return []; + let joins = ''; + for (let i = 0; i < nodeEntries.length; i++) { + const [nodeId, node] = nodeEntries[i]; + if (node.joins) { + node.joins.forEach(j => { + joins += ` INNER JOIN ${allUniqueNodes[j.to].label} ${j.to} ON ${j.from}.${j.leftJoin}::text = ${j.to}.${j.rightJoin}::text`; + }); + } } - const edge = ` - SELECT '${node.relation.label}' AS label, COUNT(*) AS count - FROM ${node.label} ${node.id} - JOIN ${node.relation.node.label} ${node.relation.node.id} - ON ${node.id}.${node.relation.label.split('.')[0]} = ${node.relation.node.id}.${node.relation.label.split('.')[1]} - GROUP BY label`; - const ret = cleanStringOfNewLinesAndTabs(edge); - return [ret, ...buildEdgeCountsCTE(node.relation.node)]; -} + sql += joins; + sql += where ? ` WHERE ${where}` : ''; + const sqlNoLimit = sql; + sql += query.limit ? ` LIMIT ${query.limit}` : ''; -export function query2SQL(query: BackendQueryFormat): QueryText { - const nodesCTE = [...new Set(query.query.flatMap(path => buildNodeCTE(path.node, query.logic)))]; - const edgesCTE = [...new Set(query.query.flatMap(path => buildEdgesCTE(path.node)))]; - const nodeCountsCTE = [...new Set(query.query.flatMap(path => buildNodeCountsCTE(path.node)))]; - const edgeCountsCTE = [...new Set(query.query.flatMap(path => buildEdgeCountsCTE(path.node)))]; + const nodes_cte = `SELECT jsonb_agg(row_to_json(t)) AS nodes FROM (${nodeEntries + .map(n => `SELECT DISTINCT ${n[0]}_row AS node FROM combined`) + .join(' UNION ALL ')}) AS t`; - const mainQuery = `WITH raw_nodes AS ( - SELECT node - FROM ( - ${nodesCTE.join(' UNION ALL ')} - ) sub - ${query.limit ? `LIMIT ${query.limit}` : ''} - ), + const allUniqueEdges = Object.values(allUniqueNodes) + .map(n => + n.joins + .map(j => { + return `SELECT jsonb_build_object( + '_id', concat('${n.label}-', md5(${j.from}_row::text), '-${allUniqueNodes[j.to].label}-', md5(${j.to}_row::text)), + 'label', '${j.leftJoin}.${j.rightJoin}', + 'attributes', '{}'::jsonb, + 'from', md5(${j.from}_row::text), + 'to', md5(${j.to}_row::text) + ) AS edge + FROM combined`; + }) + .filter(s => s.length > 0) + .join(' UNION ALL '), + ) + .filter(s => s.length > 0) + .join(' UNION ALL '); + + // console.log(JSON.stringify(allUniqueNodes, null, 2)); + // const rowList = allNodes.map(n => `${n.id}_row`); + // const select = ` + // SELECT ${allNodes.map(n => `to_jsonb(${n.id}) AS ${n.id}_row`).join(', ')}, + // ${allNodes + // .filter(n => n.relation && n!.relation!.node) + // .map( + // n => + // `${n.id}.${n!.relation!.label!.split('.')[0]}::text AS ${n.id}_${n!.relation!.label!.split('.')[0]}, ${n!.relation!.node!.id}.${ + // n!.relation!.label!.split('.')[1] + // }::text AS ${n!.relation!.node!.id}_${n!.relation!.label!.split('.')[1]}`, + // ) + // .join(', ')} + // FROM ${node.label} ${node.id} + // ${allNodes + // .filter(n => n?.relation?.node?.label && n?.relation?.node?.id) + // .map( + // n => + // `INNER JOIN ${n!.relation!.node!.label} ${n!.relation!.node!.id} ON ${n.id}.${n!.relation!.label!.split('.')[0]}::text = ${ + // n!.relation!.node!.id + // }.${n!.relation!.label!.split('.')[1]}::text`, + // ) + // .join('\n')} + // ${where ? `WHERE ${where}` : ''}`; + + // const edges = buildEdgesCTE(node, rowList, pathId).join(' UNION ALL '); + + // const nodesCTE = query.query.flatMap(path => ({ ...buildNodeCTE(path, query.logic), path: path })); + // const nodeCountsCTE = [...new Set(query.query.flatMap(path => buildNodeCountsCTE(path.node)))]; + // const edgeCountsCTE = [...new Set(query.query.flatMap(path => buildEdgeCountsCTE(path.node)))]; + + const mainQuery = `WITH combined AS (${sql}), nodes_cte AS ( - SELECT jsonb_agg(node) AS nodes - FROM raw_nodes + SELECT jsonb_agg(row_to_json(t)) AS nodes FROM (${nodeEntries + .map( + ([nodeId, node]) => ` + SELECT + md5(${nodeId}_row::text) AS _id, + '${node.label}' AS label, + ${nodeId}_row - 'id' AS attributes + FROM combined`, + ) + .join(' UNION ALL ')}) AS t ), edges_cte AS ( - SELECT jsonb_agg(edge) AS edges - FROM ( - ${edgesCTE.join(' UNION ALL ')} - ) aggregated_edges + SELECT jsonb_agg(edge) AS edges + FROM ( + ${allUniqueEdges.length > 0 ? allUniqueEdges : 'SELECT null AS edge WHERE FALSE'} + ) AS e ), node_counts AS ( - SELECT label, COUNT(*) AS count + SELECT jsonb_agg(jsonb_build_object('label', label, 'count', count)) AS nodeCounts FROM ( - ${nodeCountsCTE.join(' UNION ALL ')} - ) t - GROUP BY label - ), - edge_counts AS ( - ${edgeCountsCTE.join(' UNION ALL ')} + SELECT label, COUNT(*) AS count + FROM ( + ${[...new Set(nodeEntries.map(([_, node]) => node.label))] + .map(label => `SELECT '${label}' AS label FROM ${label}`) + .join(' UNION ALL ')} + ) AS node_labels + GROUP BY label + ) AS counts ) SELECT jsonb_build_object( 'nodes', nodes_cte.nodes, 'edges', COALESCE(edges_cte.edges, '[]'::jsonb), - 'nodeCounts', COALESCE( (SELECT jsonb_agg(jsonb_build_object('label', label, 'count', count)) - FROM node_counts), '[]'::jsonb ), - 'edgeCounts', COALESCE( (SELECT jsonb_agg(jsonb_build_object('label', label, 'count', count)) - FROM edge_counts), '[]'::jsonb ) + 'nodeCounts', COALESCE((SELECT nodeCounts FROM node_counts), '[]'::jsonb) ) AS result FROM nodes_cte, edges_cte;`; + // 'nodeCounts', COALESCE( (SELECT jsonb_agg(jsonb_build_object('label', label, 'count', count)) + // FROM node_counts), '[]'::jsonb ), + // 'edgeCounts', COALESCE( (SELECT jsonb_agg(jsonb_build_object('label', label, 'count', count)) + // FROM edge_counts), '[]'::jsonb ) + // node_counts AS ( + // SELECT label, COUNT(*) AS count + // FROM ( + // ${nodesCTE.map(node => node.node).join(' UNION ALL ')} + // ) t + // GROUP BY label + // ), + // edge_counts AS ( + // ${nodesCTE.map(node => node.edges).join(' UNION ALL ')} + // ) + return { - query: mainQuery, + query: cleanStringOfNewLinesAndTabs(mainQuery), countQuery: '', }; } diff --git a/src/queryExecution/sql/queryResultParser.ts b/src/queryExecution/sql/queryResultParser.ts deleted file mode 100644 index a3dc7f67365adc5978e0ec0e934dceb95816191c..0000000000000000000000000000000000000000 --- a/src/queryExecution/sql/queryResultParser.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { log } from '../../logger'; -import type { CountQueryResultFromBackend, EdgeQueryResult, NodeQueryResult } from 'ts-common'; -import type { GraphQueryResultFromBackend } from 'ts-common'; -import { type QueryResult } from 'pg'; - -// Adjusted to handle a Postgres QueryResult object. -export function parseSQLQueryResult(result: QueryResult, returnType: 'nodelink' | 'table' = 'nodelink'): GraphQueryResultFromBackend { - // ...existing error handling code... - try { - // Extract rows if present. - switch (returnType) { - case 'nodelink': - return parseSQLNodeLinkQuery(result.rows); - case 'table': - log.error(`Table format not supported yet`); - throw new Error('Table format not supported yet'); - default: - log.error(`Error Unknown query Format`); - throw new Error('Unknown query Format'); - } - } catch (err) { - log.error(`Error executing query`, err); - throw err; - } -} - -// Adjusted to handle a Postgres QueryResult object. -export function parseCountSQLQueryResult(result: QueryResult): CountQueryResultFromBackend { - try { - const countResult: CountQueryResultFromBackend = { updatedAt: Date.now() }; - const rows = result.rows as Record<string, any>[]; - if (rows.length > 0) { - const row = rows[0]; - for (const key in row) { - if (Object.prototype.hasOwnProperty.call(row, key)) { - countResult[key] = Number(row[key]); - } - } - } - return countResult; - } catch (err) { - log.error(`Error executing query`, err); - throw err; - } -} - -// Helper function to build a graph result (nodelink) from SQL rows. -function parseSQLNodeLinkQuery(rows: Record<string, any>[]): GraphQueryResultFromBackend { - const nodes: NodeQueryResult[] = []; - const edges: EdgeQueryResult[] = []; - const seenNodes = new Set<string>(); - const seenEdges = new Set<string>(); - - for (const row of rows) { - // If the row doesn't carry a "type", assume it represents a node. - if (!row.type) { - if (!seenNodes.has(row.id)) { - nodes.push({ - _id: row.id, - label: row.name, // using "name" as the label - attributes: { ...row }, - }); - seenNodes.add(row.id); - } - } else if (row.type === 'node') { - if (!seenNodes.has(row.id)) { - nodes.push({ - _id: row.id, - label: row.label, - attributes: row.attributes, // Assumed to be a plain object. - }); - seenNodes.add(row.id); - } - } else if (row.type === 'edge') { - if (!seenEdges.has(row.id)) { - edges.push({ - _id: row.id, - label: row.label, - from: row.from, - to: row.to, - attributes: { ...row.attributes, type: row.label }, - }); - seenEdges.add(row.id); - } - } else if (row.type === 'path') { - // If a path, assume arrays "nodes" and "edges" exist. - if (Array.isArray(row.nodes)) { - for (const node of row.nodes) { - if (!seenNodes.has(node.id)) { - nodes.push({ - _id: node.id, - label: node.label, - attributes: node.attributes, - }); - seenNodes.add(node.id); - } - } - } - if (Array.isArray(row.edges)) { - for (const edge of row.edges) { - if (!seenEdges.has(edge.id)) { - edges.push({ - _id: edge.id, - label: edge.label, - from: edge.from, - to: edge.to, - attributes: { ...edge.attributes, type: edge.label }, - }); - seenEdges.add(edge.id); - } - } - } - } else { - log.warn(`Ignoring unknown row type: ${row.type}`); - } - } - - return { nodes, edges }; -} - -// Removed neo4j-specific helper functions. diff --git a/src/readers/services/sqlService.ts b/src/readers/services/sqlService.ts index fed1c5237579123845878ac49c3da0b651dda1d3..d6451e5cb62da2833ce40fc840d4120110992eaa 100644 --- a/src/readers/services/sqlService.ts +++ b/src/readers/services/sqlService.ts @@ -2,10 +2,7 @@ import { type DbConnection, type GraphQueryResultMetaFromBackend, graphQueryBack import { PgConnection } from 'ts-common/databaseConnection/postgres'; import { log } from '../../logger'; import { QUERY_CACHE_DURATION, redis } from '../../variables'; -import { parseCountCypherQueryResult, parseCypherQueryResult } from '../../queryExecution/cypher/queryResultParser'; -import { cacheCheck } from './cache'; import type { QueryText } from '../../queryExecution/model'; -import { parseCountSQLQueryResult, parseSQLQueryResult } from '../../queryExecution/sql/queryResultParser'; export const sqlQueryService = async ( db: DbConnection, @@ -30,16 +27,14 @@ export const sqlQueryService = async ( const connection = new PgConnection(db); try { - const [result, countResult] = await connection.run([queryText.query, queryText.countQuery]); - // console.log('result:', result); + const [result] = await connection.run([queryText.query]); + // console.log('result:', result.rows[0].result); // console.log('countResult:', countResult); - - const graph = parseSQLQueryResult(result); - log.info('Parsed graph:', result); - const countGraph = parseCountSQLQueryResult(countResult); + const queryResult = result.rows[0].result as GraphQueryResultMetaFromBackend; + queryResult.nodes = queryResult.nodes ?? []; // calculate metadata - const graphQueryResult = graphQueryBackend2graphQuery(graph, countGraph); + const graphQueryResult = graphQueryBackend2graphQuery(queryResult, queryResult.nodeCounts); graphQueryResult.nodeCounts.updatedAt = Date.now(); // cache result @@ -55,8 +50,8 @@ export const sqlQueryService = async ( return graphQueryResult; } catch (error) { - log.error('Error parsing query result:', queryText, error); - throw new Error('Error parsing query result'); + log.error('Error parsing sql query result:', queryText, error); + throw new Error('Error parsing sql query result'); } finally { connection.close(); }