diff --git a/src/queryExecution/sql/queryConverterSql.ts b/src/queryExecution/sql/queryConverterSql.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d91a1d313aa97f1a953666bf9986b130789c247 --- /dev/null +++ b/src/queryExecution/sql/queryConverterSql.ts @@ -0,0 +1,284 @@ +import type { AllLogicStatement, AnyStatement, BackendQueryFormat, NodeStruct, QueryStruct } from 'ts-common'; +import type { QueryText } from '../model'; +import { extractLogicSQL } from './convertLogicToSQL'; +import type { QueryCacheData } from '../cypher/converter/model'; + +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 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; +// } + +// 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 loopThoughNodes = (node: NodeStruct): NodeStruct[] => { + if (node.relation && node.relation.node) { + return [node, ...loopThoughNodes(node.relation.node)]; + } + 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, + }); + } + }); + + console.log(JSON.stringify(allNodes, null, 2)); + + 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(', '), + ) + .filter(s => s.length > 0); + const select = [...nodeEntries.map(n => `to_jsonb(${n[0]}) AS ${n[0]}_row`), ...relIDSql].join(', '); + + 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]}`; + + 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`; + }); + } + } + + sql += joins; + sql += where ? ` WHERE ${where}` : ''; + const sqlNoLimit = sql; + sql += query.limit ? ` LIMIT ${query.limit}` : ''; + + 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 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(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 ( + ${allUniqueEdges.length > 0 ? allUniqueEdges : 'SELECT null AS edge WHERE FALSE'} + ) AS e + ), + node_counts AS ( + SELECT jsonb_agg(jsonb_build_object('label', label, 'count', count)) AS nodeCounts + FROM ( + 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 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: cleanStringOfNewLinesAndTabs(mainQuery), + countQuery: '', + }; +} diff --git a/src/utils/cypher/converter/logic.ts b/src/utils/cypher/converter/logic.ts index 4ef8a20d99ef05f57ee9f73d7259d90a3eeaf4c0..9d6a2133e94f75ab9d25e72050586118bf7b02d7 100644 --- a/src/utils/cypher/converter/logic.ts +++ b/src/utils/cypher/converter/logic.ts @@ -1,6 +1,9 @@ -import { StringFilterTypes, type AnyStatement } from 'ts-common/src/model/query/logic/general'; +import { type AllLogicStatement, type AnyStatement } from 'ts-common/src/model/query/logic/general'; import type { QueryCacheData } from './model'; import { log } from 'ts-common/src/logger/logger'; +import { NumberAggregationTypes, NumberFilterTypes, NumberFunctionTypes } from 'ts-common/src/model/query/logic/number'; +import { StringFilterTypes, StringFunctionTypes } from 'ts-common/src/model/query/logic/string'; +import { DateFilterTypes, DateFunctionTypes } from 'ts-common/src/model/query/logic/date/types'; export function createWhereLogic( op: string, @@ -8,7 +11,8 @@ export function createWhereLogic( whereLogic: Set<string>, cacheData: QueryCacheData, ): { logic: string; where: Set<string> } { - const newWhereLogic = `${left.replace('.', '_')}_${op}`; + const newOp = op.replace('_', '').toLowerCase(); + const newWhereLogic = `${left.replace('.', '_')}_${newOp}`; // TODO: Relation temporarily ignored due to unnecessary added complexity in the query // for (const relation of cacheData.relations) { @@ -17,7 +21,7 @@ export function createWhereLogic( // } // } - whereLogic.add(`${op}(${left}) AS ${newWhereLogic}`); + whereLogic.add(`${newOp}(${left}) AS ${newWhereLogic}`); for (const entity of cacheData.entities) { if (entity.id !== left) { whereLogic.add(entity.id); @@ -31,24 +35,28 @@ export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCac switch (typeof logicQuery) { case 'object': if (Array.isArray(logicQuery)) { - let op = logicQuery[0].replace('_', '').toLowerCase(); - let right = logicQuery?.[2]; + const op = logicQuery[0]; + let newOp: string = op; + const right = logicQuery?.[2]; const { logic: left, where: whereLogic } = extractLogicCypher(logicQuery[1], cacheData); switch (op) { - case '!=': - op = '<>'; + case NumberFilterTypes.EQUAL: + case StringFilterTypes.EQUAL: + case DateFilterTypes.EQUAL: + newOp = '='; break; - case '==': - if (right === '".*"') op = '=~'; // in case "equals to <empty string>", this is what happens - else op = '='; + case NumberFilterTypes.NOT_EQUAL: + case StringFilterTypes.NOT_EQUAL: + case DateFilterTypes.NOT_EQUAL: + if (right === '".*"') newOp = '=~'; // in case "equals to <empty string>", this is what happens + else newOp = '<>'; break; - case 'like': - op = '=~'; + case StringFilterTypes.LIKE: + newOp = '=~'; break; - case 'in': - case 'present in list': - op = 'IN'; + case StringFilterTypes.IN: + newOp = 'IN'; if (typeof right === 'string') { return { logic: `(${left} IN [${right @@ -60,9 +68,8 @@ export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCac }; } break; - case 'not in': - case 'not in list': - op = 'NOT IN'; + case StringFilterTypes.NOT_IN: + newOp = 'NOT IN'; if (typeof right === 'string') { return { logic: `(NOT ${left} IN [${right @@ -74,20 +81,34 @@ export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCac }; } break; - case 'isempty': + case StringFilterTypes.EMPTY: return { logic: `(${left} IS NULL OR ${left} = "")`, where: whereLogic }; - case 'isnotempty': + case StringFilterTypes.NOT_EMPTY: return { logic: `(${left} IS NOT NULL AND ${left} <> "")`, where: whereLogic }; - case 'lower': + case StringFunctionTypes.LOWER: return { logic: `toLower(${left})`, where: whereLogic }; - case 'upper': + case StringFunctionTypes.UPPER: return { logic: `toUpper(${left})`, where: whereLogic }; - case 'avg': - case 'count': - case 'max': - case 'min': - case 'sum': + case NumberAggregationTypes.AVG: + case NumberAggregationTypes.COUNT: + case NumberAggregationTypes.MAX: + case NumberAggregationTypes.MIN: + case NumberAggregationTypes.SUM: return createWhereLogic(op, left, whereLogic, cacheData); + case DateFunctionTypes.YEAR: + return { logic: `${left}.year`, where: whereLogic }; + case DateFunctionTypes.MONTH: + return { logic: `${left}.month`, where: whereLogic }; + case DateFunctionTypes.DAY: + return { logic: `${left}.day`, where: whereLogic }; + case DateFunctionTypes.HOUR: + return { logic: `${left}.hour`, where: whereLogic }; + case DateFunctionTypes.MINUTE: + return { logic: `${left}.minute`, where: whereLogic }; + case DateFunctionTypes.SECOND: + return { logic: `${left}.seconds`, where: whereLogic }; + case DateFunctionTypes.DAY_OF_WEEK: + return { logic: `${left}.weekDay`, where: whereLogic }; } if (logicQuery.length > 2 && logicQuery[2]) { const { logic: rightLogic, where: whereLogicRight } = extractLogicCypher(logicQuery[2], cacheData); @@ -96,12 +117,12 @@ export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCac for (const where of whereLogicRight) { whereLogic.add(where); } - if (op === '=~') { + if (newOp === '=~') { rightMutable = `(".*" + ${rightMutable} + ".*")`; } - return { logic: `(${left} ${op} ${rightMutable})`, where: whereLogic }; + return { logic: `(${left} ${newOp.replace('_', '').toLowerCase()} ${rightMutable})`, where: whereLogic }; } - return { logic: `(${op} ${left})`, where: whereLogic }; + return { logic: `(${newOp.replace('_', '').toLowerCase()} ${left})`, where: whereLogic }; } return { logic: logicQuery, where: new Set() }; case 'string': diff --git a/src/utils/cypher/converter/queryConverter.test.ts b/src/utils/cypher/converter/queryConverter.test.ts index 5227c6cd6ed1d867142f8bacd4718795b3702f23..de23cf8b38867e3ada042d49f2aa358c50b12b6a 100644 --- a/src/utils/cypher/converter/queryConverter.test.ts +++ b/src/utils/cypher/converter/queryConverter.test.ts @@ -1,6 +1,8 @@ +import type { BackendQueryFormat } from 'ts-common'; +import { StringFilterTypes, StringFunctionTypes } from 'ts-common/src/model/query/logic/string'; import { query2Cypher } from './queryConverter'; -import { StringFilterTypes, type BackendQueryFormat } from 'ts-common'; import { expect, test, describe, it } from 'bun:test'; +import { NumberAggregationTypes, NumberFilterTypes, NumberFunctionTypes } from 'ts-common/src/model/query/logic/number'; function fixCypherSpaces(cypher?: string | null): string { if (!cypher) { @@ -71,7 +73,7 @@ describe('query2Cypher', () => { const query: BackendQueryFormat = { saveStateID: 'test', return: ['*'], - logic: ['!=', '@p1.name', '"Raymond Campbell"'], + logic: [NumberFilterTypes.NOT_EQUAL, '@p1.name', '"Raymond Campbell"'], query: [ { id: 'path1', @@ -122,7 +124,7 @@ describe('query2Cypher', () => { const query: BackendQueryFormat = { saveStateID: 'test', limit: 5000, - logic: ['And', ['<', '@movie.imdbRating', 7.5], ['==', 'p2.age', 'p1.age']], + logic: ['And', [NumberFilterTypes.LESS_THAN, '@movie.imdbRating', 7.5], [NumberFilterTypes.EQUAL, 'p2.age', 'p1.age']], query: [ { id: 'path1', @@ -165,7 +167,7 @@ describe('query2Cypher', () => { const query: BackendQueryFormat = { saveStateID: 'test', limit: 5000, - logic: ['<', ['-', '@movie.year', 'p1.year'], 10], + logic: [NumberFilterTypes.LESS_THAN, [NumberFunctionTypes.SUBTRACT, '@movie.year', 'p1.year'], 10], query: [ { id: 'path1', @@ -199,7 +201,7 @@ describe('query2Cypher', () => { const query: BackendQueryFormat = { saveStateID: 'test', limit: 5000, - logic: ['And', ['<', '@movie.imdbRating', 7.5], ['==', 'p2.age', 'p1.age']], + logic: ['And', [NumberFilterTypes.LESS_THAN, '@movie.imdbRating', 7.5], [NumberFilterTypes.EQUAL, 'p2.age', 'p1.age']], query: [ { id: 'path1', @@ -238,7 +240,7 @@ describe('query2Cypher', () => { const query: BackendQueryFormat = { saveStateID: 'test', limit: 5000, - logic: ['<', '@p1.age', ['Avg', '@p1.age']], + logic: [NumberFilterTypes.LESS_THAN, '@p1.age', [NumberAggregationTypes.AVG, '@p1.age']], query: [ { id: 'path1', @@ -275,7 +277,7 @@ describe('query2Cypher', () => { const query: BackendQueryFormat = { saveStateID: 'test', limit: 5000, - logic: ['<', '@p1.age', ['Avg', '@p1.age']], + logic: [NumberFilterTypes.LESS_THAN, '@p1.age', [NumberAggregationTypes.AVG, '@p1.age']], query: [ { id: 'path1', @@ -330,7 +332,7 @@ describe('query2Cypher', () => { const query: BackendQueryFormat = { saveStateID: 'test', limit: 5000, - logic: ['Like', ['Lower', '@p1.name'], '"john"'], + logic: [StringFilterTypes.LIKE, [StringFunctionTypes.LOWER, '@p1.name'], '"john"'], query: [ { id: 'path1', @@ -355,7 +357,7 @@ describe('query2Cypher', () => { const query: BackendQueryFormat = { saveStateID: 'test', limit: 500, - logic: ['Like', '@id_1691576718400.title', '"ale"'], + logic: [StringFilterTypes.LIKE, '@id_1691576718400.title', '"ale"'], query: [ { id: 'path_0', @@ -386,7 +388,7 @@ describe('query2Cypher', () => { const query: BackendQueryFormat = { saveStateID: 'test', limit: 500, - logic: ['Like', '@id_1691576718400.title', '"ale"'], + logic: [StringFilterTypes.LIKE, '@id_1691576718400.title', '"ale"'], query: [ { id: 'path_0', @@ -417,7 +419,7 @@ describe('query2Cypher', () => { const query: BackendQueryFormat = { saveStateID: 'test', limit: 500, - logic: ['<', '@id_1698231933579.unitPrice', '10'], + logic: [NumberFilterTypes.LESS_THAN, '@id_1698231933579.unitPrice', '10'], query: [ { id: 'path_0', @@ -447,7 +449,7 @@ describe('query2Cypher', () => { const query: BackendQueryFormat = { saveStateID: 'test', return: ['*'], - logic: ['>', ['Count', '@p1'], '1'], + logic: [NumberFilterTypes.GREATER_THAN, [NumberAggregationTypes.COUNT, '@p1'], '1'], query: [ { id: 'path1', @@ -523,7 +525,7 @@ describe('query2Cypher', () => { }, }, ], - logic: ['Upper', '@id_1731428699410.name'], + logic: [StringFunctionTypes.UPPER, '@id_1731428699410.name'], return: ['*'], }; @@ -618,7 +620,11 @@ describe('query2Cypher', () => { limit: 100, return: ['*'], cached: false, - logic: ['And', ['==', '@id_1737115397423.GO', '"852349"'], ['==', '@id_1737115886717.goodFlow', 'false']], + logic: [ + 'And', + [NumberFilterTypes.EQUAL, '@id_1737115397423.GO', '"852349"'], + [NumberFilterTypes.EQUAL, '@id_1737115886717.goodFlow', 'false'], + ], }; const cypher = query2Cypher(query); diff --git a/src/utils/reactflow/query2backend.ts b/src/utils/reactflow/query2backend.ts index 7b797d23f1591731aebd5b0b89dbf0f3d5a86478..4625218154a816479a5cb41504ef6ca5d148c55c 100644 --- a/src/utils/reactflow/query2backend.ts +++ b/src/utils/reactflow/query2backend.ts @@ -2,7 +2,7 @@ import type { SerializedNode } from 'graphology-types'; import { hasCycle } from 'graphology-dag'; import Graph from 'graphology'; import { allSimplePaths } from 'graphology-simple-path'; -import type { BackendQueryFormat, NodeStruct, QueryStruct, RelationStruct } from 'ts-common'; +import { AllLogicMap, type BackendQueryFormat, type NodeStruct, type QueryStruct, type RelationStruct } from 'ts-common'; import type { MachineLearning } from 'ts-common/src/model/query/queryRequestModel'; import type { @@ -44,7 +44,7 @@ const traverseEntityRelationPaths = ( x: node.attributes.x, y: node.attributes.x, depth: { min: settings.depth.min, max: settings.depth.max }, - direction: 'both', + direction: QueryRelationDirection.BOTH, attributes: [], }); } else { @@ -98,9 +98,17 @@ function calculateQueryLogic( node: SerializedNode<LogicNodeAttributes>, graph: QueryMultiGraph, logics: SerializedNode<LogicNodeAttributes>[], -): AllLogicStatement { +): AllLogicStatement | undefined { if (!node?.attributes) throw Error('Malformed Graph! Node has no attributes'); const connectionsToLeft = graph.edges.filter(e => e.target === node.key); + const connectionsToRight = graph.edges.filter(e => e.source === node.key); + + const first = node.attributes.logic.logic[0]; + const logicMap = Object.values(AllLogicMap).find(value => value.type === first); + if (logicMap && logicMap.excludeFromQueryIfOutputNotConnected && connectionsToRight.length === 0) { + log.debug('AllLogicMap with ignored output due to missing third term:', first, AllLogicMap[first], node.attributes.logic.logic); + return undefined; // Return a neutral value to ignore this logic + } const ret = [...node.attributes.logic.logic].map(l => { if (typeof l !== 'string') throw Error('Malformed Graph! Logic node has no logic'); @@ -159,7 +167,7 @@ function queryLogicUnion( ): AllLogicStatement | undefined { const graphLogicChunks = nodes.map(node => calculateQueryLogic(node, graph, logics)); - if (graphLogicChunks.length === 0) return undefined; + if (graphLogicChunks.length === 0 || !graphLogicChunks[0]) return undefined; if (graphLogicChunks.length === 1) return graphLogicChunks[0]; const constraintNodeId = nodes[0].key;