From 36b497d7cc5da2bb09abf28e46c2630c66f6eb21 Mon Sep 17 00:00:00 2001
From: Leonardo <leomilho@gmail.com>
Date: Fri, 21 Feb 2025 17:05:50 +0100
Subject: [PATCH] feat: postgres

---
 bun.lockb                                     | Bin 150175 -> 158213 bytes
 package.json                                  |   8 +-
 .../arangodb/golang/README.md                 |   0
 .../arangodb/golang/executeQuery.go           |   0
 .../arangodb/golang/executeQuery_test.go      |   0
 src/queryExecution/converter.ts               |  14 +
 .../cypher/converter/export.ts                |   0
 .../cypher/converter/filter.ts                |   0
 .../cypher/converter/index.ts                 |   0
 .../cypher/converter/logic.ts                 |   0
 .../cypher/converter/model.ts                 |   0
 .../cypher/converter/node.ts                  |   0
 .../cypher/converter/queryConverter.test.ts   |   0
 .../cypher/converter/queryConverter.ts        |   9 +-
 .../cypher/converter/relation.ts              |   0
 .../cypher/queryResultParser.ts}              |   4 +-
 src/{utils => queryExecution}/hashing.ts      |   0
 src/{utils => queryExecution}/insights.ts     |   0
 src/{utils => queryExecution}/lexical.ts      |   0
 src/queryExecution/model.ts                   |   4 +
 .../queryPublisher.ts                         |   0
 .../reactflow/query2backend.ts                |   2 +-
 .../sparql/golang/README.md                   |   0
 .../sparql/golang/entity/result.go            |   0
 .../sparql/golang/executeQuery.go             |   0
 .../sparql/golang/executeQuery_test.go        |   0
 .../sql/index.ts}                             |   0
 .../sql/queryConverterSql.test.ts             | 538 ++++++++++++++++++
 src/queryExecution/sql/queryConverterSql.ts   | 233 ++++++++
 src/queryExecution/sql/queryResultParser.ts   | 121 ++++
 src/readers/diffCheck.ts                      |   2 +-
 src/readers/insightProcessor.ts               |  12 +-
 src/readers/queryService.ts                   | 106 +---
 src/readers/services/cache.ts                 |  16 +
 src/readers/services/cypherService.ts         |  57 ++
 src/readers/services/index.ts                 |  14 +
 src/readers/services/sqlService.ts            |  63 ++
 37 files changed, 1101 insertions(+), 102 deletions(-)
 rename src/{utils => queryExecution}/arangodb/golang/README.md (100%)
 rename src/{utils => queryExecution}/arangodb/golang/executeQuery.go (100%)
 rename src/{utils => queryExecution}/arangodb/golang/executeQuery_test.go (100%)
 create mode 100644 src/queryExecution/converter.ts
 rename src/{utils => queryExecution}/cypher/converter/export.ts (100%)
 rename src/{utils => queryExecution}/cypher/converter/filter.ts (100%)
 rename src/{utils => queryExecution}/cypher/converter/index.ts (100%)
 rename src/{utils => queryExecution}/cypher/converter/logic.ts (100%)
 rename src/{utils => queryExecution}/cypher/converter/model.ts (100%)
 rename src/{utils => queryExecution}/cypher/converter/node.ts (100%)
 rename src/{utils => queryExecution}/cypher/converter/queryConverter.test.ts (100%)
 rename src/{utils => queryExecution}/cypher/converter/queryConverter.ts (96%)
 rename src/{utils => queryExecution}/cypher/converter/relation.ts (100%)
 rename src/{utils/cypher/queryParser.ts => queryExecution/cypher/queryResultParser.ts} (95%)
 rename src/{utils => queryExecution}/hashing.ts (100%)
 rename src/{utils => queryExecution}/insights.ts (100%)
 rename src/{utils => queryExecution}/lexical.ts (100%)
 create mode 100644 src/queryExecution/model.ts
 rename src/{utils => queryExecution}/queryPublisher.ts (100%)
 rename src/{utils => queryExecution}/reactflow/query2backend.ts (99%)
 rename src/{utils => queryExecution}/sparql/golang/README.md (100%)
 rename src/{utils => queryExecution}/sparql/golang/entity/result.go (100%)
 rename src/{utils => queryExecution}/sparql/golang/executeQuery.go (100%)
 rename src/{utils => queryExecution}/sparql/golang/executeQuery_test.go (100%)
 rename src/{utils/cypher/queryTranslator.ts => queryExecution/sql/index.ts} (100%)
 create mode 100644 src/queryExecution/sql/queryConverterSql.test.ts
 create mode 100644 src/queryExecution/sql/queryConverterSql.ts
 create mode 100644 src/queryExecution/sql/queryResultParser.ts
 create mode 100644 src/readers/services/cache.ts
 create mode 100644 src/readers/services/cypherService.ts
 create mode 100644 src/readers/services/index.ts
 create mode 100644 src/readers/services/sqlService.ts

diff --git a/bun.lockb b/bun.lockb
index 94886d7ec58aab76974728e32acbce8e5b53710b..f9836791a740c45435f89898fd723685ca7438bd 100755
GIT binary patch
delta 30420
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
ZMK!TD|H(#_vb$T0!)s-C@e>0}|1TP~p#lH^

delta 25621
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
P&x}yiwcid^>^1)n8sFK-

diff --git a/package.json b/package.json
index 548cf5c..ffef0f1 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,9 @@
   "dependencies": {
     "@lexical/headless": "0.21.0",
     "@lexical/html": "0.21.0",
+    "@types/pg": "^8.11.11",
+    "canvas": "^3.0.0-rc3",
+    "d3": "^7.9.0",
     "graphology": "^0.25.4",
     "graphology-dag": "^0.4.1",
     "graphology-layout": "^0.6.1",
@@ -37,10 +40,9 @@
     "lexical": "0.21.0",
     "neo4j-driver": "5.26.0",
     "nodemailer": "^6.9.16",
-    "canvas": "^3.0.0-rc3",
-    "d3": "^7.9.0",
-    "svg2img": "^1.0.0-beta.2",
+    "pg": "^8.13.3",
     "plotly.js-dist-min": "^2.35.3",
+    "svg2img": "^1.0.0-beta.2",
     "ts-common": "link:ts-common"
   }
 }
diff --git a/src/utils/arangodb/golang/README.md b/src/queryExecution/arangodb/golang/README.md
similarity index 100%
rename from src/utils/arangodb/golang/README.md
rename to src/queryExecution/arangodb/golang/README.md
diff --git a/src/utils/arangodb/golang/executeQuery.go b/src/queryExecution/arangodb/golang/executeQuery.go
similarity index 100%
rename from src/utils/arangodb/golang/executeQuery.go
rename to src/queryExecution/arangodb/golang/executeQuery.go
diff --git a/src/utils/arangodb/golang/executeQuery_test.go b/src/queryExecution/arangodb/golang/executeQuery_test.go
similarity index 100%
rename from src/utils/arangodb/golang/executeQuery_test.go
rename to src/queryExecution/arangodb/golang/executeQuery_test.go
diff --git a/src/queryExecution/converter.ts b/src/queryExecution/converter.ts
new file mode 100644
index 0000000..1746523
--- /dev/null
+++ b/src/queryExecution/converter.ts
@@ -0,0 +1,14 @@
+import type { BackendQueryFormat } from 'ts-common';
+import type { QueryText } from './model';
+import { query2Cypher } from './cypher/converter';
+import { query2SQL } from './sql/queryConverterSql';
+import type { DatabaseType } from 'ts-common/src/model/webSocket/dbConnection';
+
+export const queryConverter = (JSONQuery: BackendQueryFormat, dbType: DatabaseType): QueryText => {
+  if (dbType === 'neo4j' || dbType === 'memgraph') {
+    return query2Cypher(JSONQuery);
+  } else if (dbType === 'postgres') {
+    return query2SQL(JSONQuery);
+  }
+  throw new Error('Unsupported database type');
+};
diff --git a/src/utils/cypher/converter/export.ts b/src/queryExecution/cypher/converter/export.ts
similarity index 100%
rename from src/utils/cypher/converter/export.ts
rename to src/queryExecution/cypher/converter/export.ts
diff --git a/src/utils/cypher/converter/filter.ts b/src/queryExecution/cypher/converter/filter.ts
similarity index 100%
rename from src/utils/cypher/converter/filter.ts
rename to src/queryExecution/cypher/converter/filter.ts
diff --git a/src/utils/cypher/converter/index.ts b/src/queryExecution/cypher/converter/index.ts
similarity index 100%
rename from src/utils/cypher/converter/index.ts
rename to src/queryExecution/cypher/converter/index.ts
diff --git a/src/utils/cypher/converter/logic.ts b/src/queryExecution/cypher/converter/logic.ts
similarity index 100%
rename from src/utils/cypher/converter/logic.ts
rename to src/queryExecution/cypher/converter/logic.ts
diff --git a/src/utils/cypher/converter/model.ts b/src/queryExecution/cypher/converter/model.ts
similarity index 100%
rename from src/utils/cypher/converter/model.ts
rename to src/queryExecution/cypher/converter/model.ts
diff --git a/src/utils/cypher/converter/node.ts b/src/queryExecution/cypher/converter/node.ts
similarity index 100%
rename from src/utils/cypher/converter/node.ts
rename to src/queryExecution/cypher/converter/node.ts
diff --git a/src/utils/cypher/converter/queryConverter.test.ts b/src/queryExecution/cypher/converter/queryConverter.test.ts
similarity index 100%
rename from src/utils/cypher/converter/queryConverter.test.ts
rename to src/queryExecution/cypher/converter/queryConverter.test.ts
diff --git a/src/utils/cypher/converter/queryConverter.ts b/src/queryExecution/cypher/converter/queryConverter.ts
similarity index 96%
rename from src/utils/cypher/converter/queryConverter.ts
rename to src/queryExecution/cypher/converter/queryConverter.ts
index b4b2634..a0ee7ef 100644
--- a/src/utils/cypher/converter/queryConverter.ts
+++ b/src/queryExecution/cypher/converter/queryConverter.ts
@@ -8,15 +8,10 @@ import { extractLogicCypher } from './logic';
 import { extractExportCypher } from './export';
 import type { QueryCacheData } from './model';
 import { getNodeCypher } from './node';
-import { log } from 'ts-common/src/logger/logger';
-
-export type QueryCypher = {
-  query: string;
-  countQuery: string;
-};
+import type { QueryText } from '../../model';
 
 // formQuery uses the hierarchy to create cypher for each part of the query in the right order
-export function query2Cypher(JSONQuery: BackendQueryFormat): QueryCypher {
+export function query2Cypher(JSONQuery: BackendQueryFormat): QueryText {
   let totalQuery = '';
   let matchQuery = '';
   let cacheData: QueryCacheData = { entities: [], relations: [], unwinds: [] };
diff --git a/src/utils/cypher/converter/relation.ts b/src/queryExecution/cypher/converter/relation.ts
similarity index 100%
rename from src/utils/cypher/converter/relation.ts
rename to src/queryExecution/cypher/converter/relation.ts
diff --git a/src/utils/cypher/queryParser.ts b/src/queryExecution/cypher/queryResultParser.ts
similarity index 95%
rename from src/utils/cypher/queryParser.ts
rename to src/queryExecution/cypher/queryResultParser.ts
index bb5c9bc..67156e3 100644
--- a/src/utils/cypher/queryParser.ts
+++ b/src/queryExecution/cypher/queryResultParser.ts
@@ -16,7 +16,7 @@ import { log } from '../../logger';
 import type { CountQueryResultFromBackend, EdgeQueryResult, NodeAttributes, NodeQueryResult } from 'ts-common';
 import type { GraphQueryResultFromBackend } from 'ts-common';
 
-export function parseCypherQuery(result: RecordShape[], returnType: 'nodelink' | 'table' = 'nodelink'): GraphQueryResultFromBackend {
+export function parseCypherQueryResult(result: RecordShape[], returnType: 'nodelink' | 'table' = 'nodelink'): GraphQueryResultFromBackend {
   try {
     try {
       switch (returnType) {
@@ -40,7 +40,7 @@ export function parseCypherQuery(result: RecordShape[], returnType: 'nodelink' |
     throw err;
   }
 }
-export function parseCountCypherQuery(result: RecordShape[]): CountQueryResultFromBackend {
+export function parseCountCypherQueryResult(result: RecordShape[]): CountQueryResultFromBackend {
   try {
     const countResult: CountQueryResultFromBackend = { updatedAt: Date.now() };
     for (let i = 0; i < result.length; i++) {
diff --git a/src/utils/hashing.ts b/src/queryExecution/hashing.ts
similarity index 100%
rename from src/utils/hashing.ts
rename to src/queryExecution/hashing.ts
diff --git a/src/utils/insights.ts b/src/queryExecution/insights.ts
similarity index 100%
rename from src/utils/insights.ts
rename to src/queryExecution/insights.ts
diff --git a/src/utils/lexical.ts b/src/queryExecution/lexical.ts
similarity index 100%
rename from src/utils/lexical.ts
rename to src/queryExecution/lexical.ts
diff --git a/src/queryExecution/model.ts b/src/queryExecution/model.ts
new file mode 100644
index 0000000..e627469
--- /dev/null
+++ b/src/queryExecution/model.ts
@@ -0,0 +1,4 @@
+export type QueryText = {
+  query: string;
+  countQuery: string;
+};
diff --git a/src/utils/queryPublisher.ts b/src/queryExecution/queryPublisher.ts
similarity index 100%
rename from src/utils/queryPublisher.ts
rename to src/queryExecution/queryPublisher.ts
diff --git a/src/utils/reactflow/query2backend.ts b/src/queryExecution/reactflow/query2backend.ts
similarity index 99%
rename from src/utils/reactflow/query2backend.ts
rename to src/queryExecution/reactflow/query2backend.ts
index 7b797d2..ef71c5c 100644
--- a/src/utils/reactflow/query2backend.ts
+++ b/src/queryExecution/reactflow/query2backend.ts
@@ -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 {
diff --git a/src/utils/sparql/golang/README.md b/src/queryExecution/sparql/golang/README.md
similarity index 100%
rename from src/utils/sparql/golang/README.md
rename to src/queryExecution/sparql/golang/README.md
diff --git a/src/utils/sparql/golang/entity/result.go b/src/queryExecution/sparql/golang/entity/result.go
similarity index 100%
rename from src/utils/sparql/golang/entity/result.go
rename to src/queryExecution/sparql/golang/entity/result.go
diff --git a/src/utils/sparql/golang/executeQuery.go b/src/queryExecution/sparql/golang/executeQuery.go
similarity index 100%
rename from src/utils/sparql/golang/executeQuery.go
rename to src/queryExecution/sparql/golang/executeQuery.go
diff --git a/src/utils/sparql/golang/executeQuery_test.go b/src/queryExecution/sparql/golang/executeQuery_test.go
similarity index 100%
rename from src/utils/sparql/golang/executeQuery_test.go
rename to src/queryExecution/sparql/golang/executeQuery_test.go
diff --git a/src/utils/cypher/queryTranslator.ts b/src/queryExecution/sql/index.ts
similarity index 100%
rename from src/utils/cypher/queryTranslator.ts
rename to src/queryExecution/sql/index.ts
diff --git a/src/queryExecution/sql/queryConverterSql.test.ts b/src/queryExecution/sql/queryConverterSql.test.ts
new file mode 100644
index 0000000..ec9d796
--- /dev/null
+++ b/src/queryExecution/sql/queryConverterSql.test.ts
@@ -0,0 +1,538 @@
+import type { BackendQueryFormat } from 'ts-common';
+import { expect, test, describe, it } from 'bun:test';
+import { query2SQL } from './queryConverterSql';
+
+function fixSQLSpaces(sql?: string | null): string {
+  if (!sql) {
+    return '';
+  }
+  let trimmedSQL = sql.replace(/\n/g, ' ');
+  trimmedSQL = trimmedSQL.replaceAll(/ {2,50}/g, ' ');
+  trimmedSQL = trimmedSQL.replace(/\t+/g, '');
+  return trimmedSQL.trim();
+}
+
+describe('query2SQL', () => {
+  it('should return correctly on a simple query with multiple paths', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      return: ['*'],
+      query: [
+        {
+          id: 'path1',
+          node: {
+            label: 'Person',
+            id: 'p1',
+            relation: {
+              label: 'name.director',
+              direction: 'TO',
+              depth: { min: 1, max: 1 },
+              node: {
+                label: 'Movie',
+                id: 'm1',
+              },
+            },
+          },
+        },
+        {
+          id: 'path2',
+          node: {
+            label: 'Person',
+            id: 'p1',
+            relation: {
+              label: 'name.person',
+              direction: 'TO',
+              depth: { min: 1, max: 1 },
+              node: {
+                label: 'Genre',
+                id: 'g1',
+              },
+            },
+          },
+        },
+      ],
+      limit: 5000,
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `SELECT * FROM Person p1 
+      JOIN Movie m1 ON p1.name = m1.director
+      JOIN Genre g1 ON p1.name = g1.person
+      LIMIT 5000;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+
+  it('should return correctly on a complex query with logic', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      return: ['*'],
+      logic: ['!=', '@p1.name', '"Raymond Campbell"'],
+      query: [
+        {
+          id: 'path1',
+          node: {
+            label: 'Person',
+            id: 'p1',
+            relation: {
+              label: 'watched.id',
+              direction: 'TO',
+              depth: { min: 1, max: 1 },
+              node: {
+                label: 'Movie',
+                id: 'm1',
+              },
+            },
+          },
+        },
+        {
+          id: 'path2',
+          node: {
+            label: 'Person',
+            id: 'p1',
+            relation: {
+              label: 'watched_genre.id',
+              direction: 'TO',
+              depth: { min: 1, max: 1 },
+              node: {
+                label: 'Genre',
+                id: 'g1',
+              },
+            },
+          },
+        },
+      ],
+      limit: 5000,
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `SELECT * FROM Person p1 
+      JOIN Movie m1 ON p1.watched = m1.id
+      JOIN Genre g1 ON p1.watched_genre = g1.id
+      WHERE p1.name <> 'Raymond Campbell'
+      LIMIT 5000;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+
+  //   it('should return correctly on a query with group by logic', () => {
+  //     const query: BackendQueryFormat = {
+  //       saveStateID: 'test',
+  //       limit: 5000,
+  //       logic: ['And', ['<', '@movie.imdbRating', 7.5], ['!=', 'p2.age', 'p1.age']],
+  //       query: [
+  //         {
+  //           id: 'path1',
+  //           node: {
+  //             label: 'Person',
+  //             id: 'p1',
+  //             relation: {
+  //               id: 'doesnotmatter',
+  //               label: 'acted.name',
+  //               depth: { min: 1, max: 1 },
+  //               direction: 'TO',
+  //               node: {
+  //                 label: 'Movie',
+  //                 id: 'movie',
+  //               },
+  //             },
+  //           },
+  //         },
+  //         {
+  //           id: 'path2',
+  //           node: {
+  //             label: 'Person',
+  //             id: 'p2',
+  //           },
+  //         },
+  //       ],
+  //       return: ['@path2'],
+  //     };
+
+  //     const sql = query2SQL(query);
+  //     const expectedSQL = `SELECT p2.* FROM Person p1
+  //       JOIN Movie movie ON p1.acted = movie.name
+  //       JOIN Person AS p2 ON p2.id IS NOT NULL
+  //       WHERE movie.imdbRating < 7.5 AND p2.age = p1.age
+  //       LIMIT 5000;`;
+
+  //     expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  //   });
+
+  it('should return correctly on a query with no label', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      limit: 5000,
+      logic: ['<', ['-', '@movie.year', '@p1.year'], 10],
+      query: [
+        {
+          id: 'path1',
+          node: {
+            id: 'p1',
+            filter: [],
+            relation: {
+              id: 'asdasd',
+              label: 'acted.id',
+              depth: { min: 1, max: 1 },
+              direction: 'TO',
+              node: {
+                label: 'Movie',
+                id: 'movie',
+              },
+            },
+          },
+        },
+      ],
+      return: ['*'],
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `SELECT * FROM p1 
+      JOIN Movie movie ON p1.acted = movie.id
+      WHERE (movie.year - p1.year) < 10
+      LIMIT 5000;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+
+  //   it('should return correctly on a query with no depth', () => {
+  //     const query: BackendQueryFormat = {
+  //       saveStateID: 'test',
+  //       limit: 5000,
+  //       logic: ['And', ['<', '@movie.imdbRating', 7.5], ['==', 'p2.age', 'p1.age']],
+  //       query: [
+  //         {
+  //           id: 'path1',
+  //           node: {
+  //             id: 'p1',
+  //             relation: {
+  //               id: 'acted',
+  //               direction: 'TO',
+  //               node: {
+  //                 label: 'Movie',
+  //                 id: 'movie',
+  //               },
+  //             },
+  //           },
+  //         },
+  //         {
+  //           id: 'path2',
+  //           node: {
+  //             id: 'p2',
+  //           },
+  //         },
+  //       ],
+  //       return: ['*'],
+  //     };
+
+  //     const sql = query2SQL(query);
+  //     const expectedSQL = `SELECT * FROM p1
+  //       JOIN Movie movie ON p1.id = movie.id AND movie.relation = 'acted'
+  //       JOIN p2 ON /* join condition assumed */
+  //       WHERE movie.imdbRating < 7.5 AND p2.age = p1.age
+  //       LIMIT 5000;`;
+
+  //     expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  //   });
+
+  it('should return correctly on a query with average calculation', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      limit: 5000,
+      logic: ['<', '@p1.age', ['Avg', '@p1.age']],
+      query: [
+        {
+          id: 'path1',
+          node: {
+            label: 'Person',
+            id: 'p1',
+            relation: {
+              id: 'acted',
+              label: 'acted.id',
+              depth: { min: 1, max: 1 },
+              direction: 'TO',
+              node: {
+                label: 'Movie',
+                id: 'movie',
+              },
+            },
+          },
+        },
+      ],
+      return: ['*'],
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `WITH (SELECT AVG(p1.age) FROM Person p1 
+      JOIN Movie movie ON p1.acted = movie.id) AS p1_age_avg
+      SELECT * FROM Person p1 
+      JOIN Movie movie ON p1.acted = movie.id
+      WHERE p1.age < p1_age_avg
+      LIMIT 5000;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+
+  it('should return correctly on a query with average calculation and multiple paths', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      limit: 5000,
+      logic: ['<', '@p1.age', ['Avg', '@p1.age']],
+      query: [
+        {
+          id: 'path1',
+          node: {
+            label: 'Person',
+            id: 'p1',
+            relation: {
+              id: 'someid',
+              label: 'acted.id',
+              depth: { min: 1, max: 1 },
+              direction: 'TO',
+              node: {
+                label: 'Movie',
+                id: 'movie',
+              },
+            },
+          },
+        },
+        {
+          id: 'path2',
+          node: {
+            label: 'Person',
+            id: 'p2',
+            relation: {
+              id: 'acted',
+              label: 'acted.id',
+              depth: { min: 1, max: 1 },
+              direction: 'TO',
+              node: {
+                label: 'Movie',
+                id: 'movie',
+              },
+            },
+          },
+        },
+      ],
+      return: ['*'],
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `WITH (SELECT AVG(p1.age) FROM Person p1 
+      JOIN Movie movie ON p1.id = movie.id AND movie.relation = 'ACTED_IN') AS p1_age_avg
+      SELECT * FROM Person p1 
+      JOIN Movie movie ON p1.id = movie.id AND movie.relation = 'ACTED_IN'
+      JOIN Person p2 ON /* join condition assumed */
+      WHERE p1.age < p1_age_avg
+      LIMIT 5000;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+
+  it('should return correctly on a single entity query with lower like logic', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      limit: 5000,
+      logic: ['Like', ['Lower', '@p1.name'], '"john"'],
+      query: [
+        {
+          id: 'path1',
+          node: {
+            label: 'Person',
+            id: 'p1',
+          },
+        },
+      ],
+      return: ['*'],
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `SELECT * FROM Person p1
+      WHERE LOWER(p1.name) LIKE '%john%'
+      LIMIT 5000;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+
+  it('should return correctly on a query with like logic', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      limit: 500,
+      logic: ['Like', '@id_1691576718400.title', '"ale"'],
+      query: [
+        {
+          id: 'path_0',
+          node: {
+            id: 'id_1691576718400',
+            label: 'Employee',
+            relation: {
+              id: 'id_1691576720177',
+              label: 'REPORTS_TO',
+              direction: 'TO',
+              node: {},
+            },
+          },
+        },
+      ],
+      return: ['*'],
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `SELECT * FROM Employee id_1691576718400
+      WHERE id_1691576718400.title LIKE '%ale%'
+      LIMIT 500;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+
+  it('should return correctly on a query with both direction relation', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      limit: 500,
+      logic: ['Like', '@id_1691576718400.title', '"ale"'],
+      query: [
+        {
+          id: 'path_0',
+          node: {
+            id: 'id_1691576718400',
+            label: 'Employee',
+            relation: {
+              id: 'id_1691576720177',
+              label: 'REPORTS_TO',
+              direction: 'BOTH',
+              node: {},
+            },
+          },
+        },
+      ],
+      return: ['*'],
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `SELECT * FROM Employee id_1691576718400
+      WHERE id_1691576718400.title LIKE '%ale%'
+      LIMIT 500;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+
+  it('should return correctly on a query with relation logic', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      limit: 500,
+      logic: ['<', '@id_1698231933579.unitPrice', '10'],
+      query: [
+        {
+          id: 'path_0',
+          node: {
+            relation: {
+              id: 'id_1698231933579',
+              label: 'CONTAINS',
+              depth: { min: 0, max: 1 },
+              direction: 'TO',
+              node: {},
+            },
+          },
+        },
+      ],
+      return: ['*'],
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `SELECT * FROM CONTAINS
+      WHERE unitPrice < 10
+      LIMIT 500;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+
+  it('should return correctly on a query with count logic', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      return: ['*'],
+      logic: ['>', ['Count', '@p1'], '1'],
+      query: [
+        {
+          id: 'path1',
+          node: {
+            label: 'Person',
+            id: 'p1',
+            relation: {
+              label: 'DIRECTED',
+              direction: 'TO',
+              depth: { min: 1, max: 1 },
+              node: {
+                label: 'Movie',
+                id: 'm1',
+              },
+            },
+          },
+        },
+      ],
+      limit: 5000,
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `WITH (SELECT COUNT(p1.id) FROM Person p1 
+      JOIN Movie m1 ON p1.id = m1.id AND m1.relation = 'DIRECTED') AS p1_count
+      SELECT * FROM Person p1
+      JOIN Movie m1 ON p1.id = m1.id AND m1.relation = 'DIRECTED'
+      WHERE p1_count > 1
+      LIMIT 5000;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+
+  it('should return correctly on a query with empty relation', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      limit: 500,
+      query: [
+        {
+          id: 'path_0',
+          node: {
+            label: 'Movie',
+            id: 'id_1730483610947',
+            relation: {
+              label: '',
+              id: '',
+              depth: { min: 0, max: 0 },
+            },
+          },
+        },
+      ],
+      return: ['*'],
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `SELECT * FROM Movie id_1730483610947
+      LIMIT 500;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+
+  it('should return correctly on a query with upper case logic', () => {
+    const query: BackendQueryFormat = {
+      saveStateID: 'test',
+      limit: 500,
+      query: [
+        {
+          id: 'path_0',
+          node: {
+            id: 'id_1731428699410',
+            label: 'Character',
+          },
+        },
+      ],
+      logic: ['Upper', '@id_1731428699410.name'],
+      return: ['*'],
+    };
+
+    const sql = query2SQL(query);
+    const expectedSQL = `SELECT * FROM Character id_1731428699410
+      WHERE UPPER(id_1731428699410.name) IS NOT NULL
+      LIMIT 500;`;
+
+    expect(fixSQLSpaces(sql.query)).toBe(fixSQLSpaces(expectedSQL));
+  });
+});
diff --git a/src/queryExecution/sql/queryConverterSql.ts b/src/queryExecution/sql/queryConverterSql.ts
new file mode 100644
index 0000000..225b7d1
--- /dev/null
+++ b/src/queryExecution/sql/queryConverterSql.ts
@@ -0,0 +1,233 @@
+import type { BackendQueryFormat } from 'ts-common';
+import type { QueryText } from '../model';
+
+type Logic = any;
+
+export function query2SQL(query: BackendQueryFormat): QueryText {
+  const { saveStateID, return: returnFields, query: paths, limit, logic } = query;
+
+  let selectFields = '*';
+  if (returnFields && returnFields.length > 0) {
+    selectFields = returnFields.join(', ');
+  }
+
+  let sqlQuery = `SELECT ${selectFields} FROM `;
+  let countQuery = `SELECT `;
+  const joins: string[] = [];
+  const whereConditions: string[] = [];
+  const countFields: string[] = [];
+  let baseTable = '';
+  let baseAlias = '';
+
+  if (paths && paths.length > 0) {
+    paths.forEach((path, index) => {
+      if (path.node) {
+        const { label, id, relation } = path.node;
+        const tableName = label || id || 'unknown';
+        // Set tableAlias to id if no label exists, else use id first if present.
+        const tableAlias = label ? id || label : id || `table${index}`;
+
+        if (index === 0) {
+          // If no label, output only one token.
+          sqlQuery = `SELECT ${selectFields} FROM ${tableName === tableAlias ? tableName : tableName + ' ' + tableAlias}`;
+          baseTable = tableName;
+          baseAlias = tableName === tableAlias ? '' : tableAlias;
+          // Push count for base node.
+          countFields.push(`COUNT(${baseAlias || baseTable}.id) as ${baseAlias || baseTable}_count`);
+        }
+
+        if (relation) {
+          // Use relation.label if provided; otherwise, use relation.id.
+          const relString = relation.label || relation.id;
+          if (relString) {
+            const relatedNode = relation.node;
+            if (relatedNode) {
+              const relatedTableAlias = relatedNode.id || relatedNode.label;
+              if (relation.direction === 'TO') {
+                if (relString.indexOf('.') > -1) {
+                  const parts = relString.split('.');
+                  const baseField = parts[0];
+                  const relatedField = parts[1];
+                  joins.push(
+                    `JOIN ${relatedNode.label} ${relatedTableAlias} ON ${tableAlias}.${baseField} = ${relatedTableAlias}.${relatedField}`,
+                  );
+                } else {
+                  joins.push(
+                    `JOIN ${relatedNode.label} ${relatedTableAlias} ON ${tableAlias}.id = ${relatedTableAlias}.id AND ${relatedTableAlias}.relation = '${relString}'`,
+                  );
+                }
+                // Push count for the related node.
+                countFields.push(`COUNT(${relatedTableAlias}.id) as ${relatedTableAlias}_count`);
+              }
+            }
+          }
+        }
+        // Do not push duplicate count for base node for subsequent paths.
+      } else {
+        sqlQuery = `SELECT * FROM ${path.id}`;
+      }
+    });
+  }
+
+  // Special handling for average calculation logic.
+  if (logic && Array.isArray(logic) && logic[0] === '<' && Array.isArray(logic[2]) && logic[2][0] === 'Avg') {
+    // Extract field from left operand: e.g. '@p1.age' -> 'p1.age'
+    const leftOperand = logic[1];
+    const field = String(leftOperand).startsWith('@') ? String(leftOperand).substring(1) : leftOperand;
+    const parts = String(field).split('.');
+    const alias = `${parts[0]}_${parts[1]}_avg`;
+    // Build WITH clause using the base table and joins.
+    const withClause = `WITH (SELECT AVG(${field}) FROM ${baseTable} ${baseAlias || baseTable}${
+      joins.length > 0 ? ' ' + joins.join(' ') : ''
+    }) AS ${alias}`;
+    // Prepend the WITH clause to the main query and append join clauses to main query.
+    sqlQuery = withClause + ' ' + sqlQuery + (joins.length > 0 ? ' ' + joins.join(' ') : '');
+    sqlQuery += ` WHERE ${field} < ${alias}`;
+    if (limit) {
+      sqlQuery += ` LIMIT ${limit}`;
+    }
+    return { query: sqlQuery + ';', countQuery: '' };
+  }
+
+  if (logic) {
+    const whereClause = translateLogicToSQL(logic);
+    if (whereClause) {
+      whereConditions.push(whereClause);
+    }
+  }
+
+  if (joins.length > 0) {
+    sqlQuery += ` ${joins.join(' ')}`;
+  }
+
+  if (whereConditions.length > 0) {
+    sqlQuery += ` WHERE ${whereConditions.join(' AND ').replace(/@/g, '').replace(/"/g, "'")}`;
+  }
+
+  if (limit) {
+    sqlQuery += ` LIMIT ${limit}`;
+  }
+
+  if (countFields.length > 0) {
+    countQuery += countFields.join(', ');
+    countQuery += ` FROM ${baseTable} ${baseAlias}`;
+    // Append join clauses for count query
+    if (joins.length > 0) {
+      countQuery += ` ${joins.join(' ')}`;
+    }
+    return { query: sqlQuery + ';', countQuery: countQuery + ';' };
+  }
+
+  return { query: sqlQuery, countQuery: '' };
+}
+
+function translateLogicToSQL(logic: Logic): string | null {
+  if (typeof logic === 'string' || typeof logic === 'number' || typeof logic === 'boolean') {
+    return String(logic);
+  }
+
+  if (Array.isArray(logic)) {
+    const operator = logic[0];
+
+    switch (operator) {
+      case 'And': {
+        const operands = logic.slice(1) as Logic[];
+        const sqlOperands = operands.map(translateLogicToSQL).filter(Boolean);
+        return sqlOperands.length > 0 ? `(${sqlOperands.join(' AND ')})` : null;
+      }
+      case 'Or': {
+        const operands = logic.slice(1) as Logic[];
+        const sqlOperands = operands.map(translateLogicToSQL).filter(Boolean);
+        return sqlOperands.length > 0 ? `(${sqlOperands.join(' OR ')})` : null;
+      }
+      case 'Not': {
+        const operand = logic[1] as Logic;
+        const sqlOperand = translateLogicToSQL(operand);
+        return sqlOperand ? `NOT (${sqlOperand})` : null;
+      }
+      case '==': {
+        const left = translateLogicToSQL(logic[1] as Logic);
+        const right = translateLogicToSQL(logic[2] as Logic);
+        return left && right ? `${left} = ${right}` : null;
+      }
+      case '!=': {
+        const left = translateLogicToSQL(logic[1] as Logic);
+        const right = translateLogicToSQL(logic[2] as Logic);
+        return left && right ? `${left} <> ${right}` : null;
+      }
+      case '<': {
+        const left = translateLogicToSQL(logic[1] as Logic);
+        const right = translateLogicToSQL(logic[2] as Logic);
+        return left && right ? `${left} < ${right}` : null;
+      }
+      case '>': {
+        const left = translateLogicToSQL(logic[1] as Logic);
+        const right = translateLogicToSQL(logic[2] as Logic);
+        return left && right ? `${left} > ${right}` : null;
+      }
+      case '<=': {
+        const left = translateLogicToSQL(logic[1] as Logic);
+        const right = translateLogicToSQL(logic[2] as Logic);
+        return left && right ? `${left} <= ${right}` : null;
+      }
+      case '>=': {
+        const left = translateLogicToSQL(logic[1] as Logic);
+        const right = translateLogicToSQL(logic[2] as Logic);
+        return left && right ? `${left} >= ${right}` : null;
+      }
+      case '+': {
+        const left = translateLogicToSQL(logic[1] as Logic);
+        const right = translateLogicToSQL(logic[2] as Logic);
+        return left && right ? `${left} + ${right}` : null;
+      }
+      case '-': {
+        const left = translateLogicToSQL(logic[1] as Logic);
+        const right = translateLogicToSQL(logic[2] as Logic);
+        return left && right ? `(${left} - ${right})` : null;
+      }
+      case '*': {
+        const left = translateLogicToSQL(logic[1] as Logic);
+        const right = translateLogicToSQL(logic[2] as Logic);
+        return left && right ? `${left} * ${right}` : null;
+      }
+      case '/': {
+        const left = translateLogicToSQL(logic[1] as Logic);
+        const right = translateLogicToSQL(logic[2] as Logic);
+        return left && right ? `${left} / ${right}` : null;
+      }
+      case 'Like': {
+        const left = translateLogicToSQL(logic[1] as Logic);
+        const right = translateLogicToSQL(logic[2] as Logic);
+        return left && right ? `${left} LIKE '%${right.replace(/"/g, '')}%'` : null;
+      }
+      case 'Lower': {
+        const operand = logic[1] as Logic;
+        const sqlOperand = translateLogicToSQL(operand);
+        return sqlOperand ? `LOWER(${sqlOperand})` : null;
+      }
+      case 'Upper': {
+        const operand = logic[1] as Logic;
+        const sqlOperand = translateLogicToSQL(operand);
+        return sqlOperand ? `UPPER(${sqlOperand}) IS NOT NULL` : null;
+      }
+      case 'Avg': {
+        const operand = logic[1] as Logic;
+        const sqlOperand = translateLogicToSQL(operand);
+        return sqlOperand ? `AVG(${sqlOperand})` : null;
+      }
+      case 'Count': {
+        const operand = logic[1] as Logic;
+        const sqlOperand = translateLogicToSQL(operand);
+        return sqlOperand ? `COUNT(${sqlOperand})` : null;
+      }
+      default:
+        return null;
+    }
+  }
+
+  if (typeof logic === 'string') {
+    return logic.startsWith('@') ? logic.substring(1).replace('.', '.') : `'${logic}'`;
+  }
+
+  return null;
+}
diff --git a/src/queryExecution/sql/queryResultParser.ts b/src/queryExecution/sql/queryResultParser.ts
new file mode 100644
index 0000000..a3dc7f6
--- /dev/null
+++ b/src/queryExecution/sql/queryResultParser.ts
@@ -0,0 +1,121 @@
+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/diffCheck.ts b/src/readers/diffCheck.ts
index ba3dd09..b5f5cfe 100644
--- a/src/readers/diffCheck.ts
+++ b/src/readers/diffCheck.ts
@@ -1,6 +1,6 @@
 import type { SaveState } from 'ts-common';
 import { log } from '../logger';
-import { hashDictionary, hashIsEqual } from '../utils/hashing';
+import { hashDictionary, hashIsEqual } from '../queryExecution/hashing';
 import type { GraphQueryResultMetaFromBackend } from 'ts-common/src/model/webSocket/graphResult';
 import { ums } from '../variables';
 import type { InsightModel } from 'ts-common';
diff --git a/src/readers/insightProcessor.ts b/src/readers/insightProcessor.ts
index 71014ea..df86921 100644
--- a/src/readers/insightProcessor.ts
+++ b/src/readers/insightProcessor.ts
@@ -4,14 +4,14 @@ import { type InsightModel } from 'ts-common';
 import { createHeadlessEditor } from '@lexical/headless';
 import { $generateHtmlFromNodes } from '@lexical/html';
 import { JSDOM } from 'jsdom';
-import { Query2BackendQuery } from '../utils/reactflow/query2backend';
-import { query2Cypher } from '../utils/cypher/converter';
-import { queryService } from './queryService';
+import { Query2BackendQuery } from '../queryExecution/reactflow/query2backend';
+import { query2Cypher } from '../queryExecution/cypher/converter';
 import { statCheck } from './statCheck';
 import { diffCheck } from './diffCheck';
-import { VariableNode } from '../utils/lexical';
-import { populateTemplate } from '../utils/insights';
+import { VariableNode } from '../queryExecution/lexical';
+import { populateTemplate } from '../queryExecution/insights';
 import { RabbitMqBroker } from 'ts-common/rabbitMq';
+import { cypherQueryService } from './services/cypherService';
 
 const dom = new JSDOM();
 function setUpDom() {
@@ -94,7 +94,7 @@ export const insightProcessor = async () => {
       const cypher = query2Cypher(convertedQuery);
       if (cypher == null) return;
       try {
-        const result = await queryService(ss.dbConnections[0], cypher, true);
+        const result = await cypherQueryService(ss.dbConnections[0], cypher, false);
 
         insight.status = false;
 
diff --git a/src/readers/queryService.ts b/src/readers/queryService.ts
index 56d4455..46063bf 100644
--- a/src/readers/queryService.ts
+++ b/src/readers/queryService.ts
@@ -1,75 +1,15 @@
-import { graphQueryBackend2graphQuery, type DbConnection, type QueryRequest } from 'ts-common';
+import { type DbConnection, type QueryRequest } from 'ts-common';
 
-import { QUERY_CACHE_DURATION, rabbitMq, redis, ums, type QueryExecutionTypes } from '../variables';
+import { rabbitMq, ums, type QueryExecutionTypes } from '../variables';
 import { log } from '../logger';
-import { QueryPublisher } from '../utils/queryPublisher';
-import { query2Cypher } from '../utils/cypher/converter';
-import { parseCountCypherQuery, parseCypherQuery } from '../utils/cypher/queryParser';
+import { QueryPublisher } from '../queryExecution/queryPublisher';
+import { query2Cypher } from '../queryExecution/cypher/converter';
+
 import { formatTimeDifference } from 'ts-common/src/logger/logger';
-import { Query2BackendQuery } from '../utils/reactflow/query2backend';
-import type { GraphQueryResultFromBackend, GraphQueryResultMetaFromBackend } from 'ts-common/src/model/webSocket/graphResult';
+import { Query2BackendQuery } from '../queryExecution/reactflow/query2backend';
 import { RabbitMqBroker } from 'ts-common/rabbitMq';
-import { Neo4jConnection } from 'ts-common/neo4j';
-import type { QueryCypher } from '../utils/cypher/converter/queryConverter';
-
-async function cacheCheck(cacheKey: string): Promise<GraphQueryResultMetaFromBackend | undefined> {
-  log.debug('Checking cache for query, with cache ttl', QUERY_CACHE_DURATION, 'seconds');
-  const cached = await redis.client.get(cacheKey);
-  if (cached) {
-    log.info('Cache hit for query');
-    const buf = Buffer.from(cached, 'base64');
-    const inflated = Bun.gunzipSync(new Uint8Array(buf));
-    const dec = new TextDecoder();
-    const cachedMessage = JSON.parse(dec.decode(inflated)) as GraphQueryResultMetaFromBackend;
-    return cachedMessage;
-  }
-}
-
-export const queryService = async (db: DbConnection, cypher: QueryCypher, useCached: boolean): Promise<GraphQueryResultMetaFromBackend> => {
-  let index = 0;
-  const disambiguatedQuery = cypher.query.replace(/\d{13}/g, () => (index++).toString());
-  const cacheKey = Bun.hash(JSON.stringify({ db: db, query: disambiguatedQuery })).toString();
-
-  if (QUERY_CACHE_DURATION === '') {
-    log.info('Query cache disabled, skipping cache check');
-  } else if (!useCached) {
-    log.info('Skipping cache check for query due to parameter', useCached);
-  } else {
-    const cachedMessage = await cacheCheck(cacheKey);
-    if (cachedMessage) {
-      log.debug('Cache hit for query', disambiguatedQuery);
-      return cachedMessage;
-    }
-  }
-
-  // TODO: only neo4j is supported for now
-  const connection = new Neo4jConnection(db);
-  try {
-    const [neo4jResult, neo4jCountResult] = await connection.run([cypher.query, cypher.countQuery]);
-    const graph = parseCypherQuery(neo4jResult.records);
-    const countGraph = parseCountCypherQuery(neo4jCountResult.records);
-
-    // calculate metadata
-    const result = graphQueryBackend2graphQuery(graph, countGraph);
-    result.nodeCounts.updatedAt = Date.now();
-
-    // cache result
-    const compressedMessage = Bun.gzipSync(JSON.stringify(result));
-    const base64Message = Buffer.from(compressedMessage).toString('base64');
-
-    if (QUERY_CACHE_DURATION !== '') {
-      // if cache enabled, cache the result
-      await redis.setWithExpire(cacheKey, base64Message, QUERY_CACHE_DURATION); // ttl in seconds
-    }
-
-    return result;
-  } catch (error) {
-    log.error('Error parsing query result:', cypher, error);
-    throw new Error('Error parsing query result');
-  } finally {
-    connection.close();
-  }
-};
+import { languageQueryService } from './services';
+import { queryConverter } from '../queryExecution/converter';
 
 export const queryServiceReader = async (frontendPublisher: RabbitMqBroker, mlPublisher: RabbitMqBroker, type: QueryExecutionTypes) => {
   if (type == null) {
@@ -157,27 +97,29 @@ export const queryServiceReader = async (frontendPublisher: RabbitMqBroker, mlPu
 
     const queryBuilderSettings = activeQueryInfo.settings; //ss.queries[0].settings;
     const ml = message.ml;
+
     const convertedQuery = Query2BackendQuery(ss.id, visualQuery, queryBuilderSettings, ml);
 
     log.debug('translating query:', convertedQuery);
     publisher.publishStatusToFrontend('Translating');
 
-    const cypher = query2Cypher(convertedQuery);
-    const query = cypher.query;
-    if (query == null) {
-      log.error('Error translating query:', convertedQuery);
-      publisher.publishErrorToFrontend('Error translating query');
-      return;
-    }
-
-    log.debug('Translated query FROM:', convertedQuery);
-    log.info('Translated query:', query);
-    log.info('Translated query:', cypher.countQuery);
-    publisher.publishTranslationResultToFrontend(query);
-
     for (let i = 0; i < ss.dbConnections.length; i++) {
       try {
-        const result = await queryService(ss.dbConnections[i], cypher, message.useCached);
+        const queryText = queryConverter(convertedQuery, ss.dbConnections[i].type);
+        // log.info('Translated query:', queryText.query);
+        log.info('Translated query:', queryText.countQuery);
+        const query = queryText.query;
+        if (query == null) {
+          log.error('Error translating query:', convertedQuery);
+          publisher.publishErrorToFrontend('Error translating query');
+          return;
+        }
+
+        log.debug('Translated query FROM:', convertedQuery);
+        log.info('Translated query:', query);
+        log.info('Translated query:', queryText.countQuery);
+
+        const result = await languageQueryService(ss.dbConnections[i], queryText, message.useCached);
 
         // Cache nodeCounts such that we can display differentiation for each query
         await ums.updateQuery(headers.message.sessionData.userID, message.saveStateID, {
diff --git a/src/readers/services/cache.ts b/src/readers/services/cache.ts
new file mode 100644
index 0000000..bc21b56
--- /dev/null
+++ b/src/readers/services/cache.ts
@@ -0,0 +1,16 @@
+import type { GraphQueryResultMetaFromBackend } from 'ts-common';
+import { log } from '../../logger';
+import { QUERY_CACHE_DURATION, redis } from '../../variables';
+
+export async function cacheCheck(cacheKey: string): Promise<GraphQueryResultMetaFromBackend | undefined> {
+  log.debug('Checking cache for query, with cache ttl', QUERY_CACHE_DURATION, 'seconds');
+  const cached = await redis.client.get(cacheKey);
+  if (cached) {
+    log.info('Cache hit for query');
+    const buf = Buffer.from(cached, 'base64');
+    const inflated = Bun.gunzipSync(new Uint8Array(buf));
+    const dec = new TextDecoder();
+    const cachedMessage = JSON.parse(dec.decode(inflated)) as GraphQueryResultMetaFromBackend;
+    return cachedMessage;
+  }
+}
diff --git a/src/readers/services/cypherService.ts b/src/readers/services/cypherService.ts
new file mode 100644
index 0000000..d138906
--- /dev/null
+++ b/src/readers/services/cypherService.ts
@@ -0,0 +1,57 @@
+import { type DbConnection, type GraphQueryResultMetaFromBackend, graphQueryBackend2graphQuery } from 'ts-common';
+import { Neo4jConnection } from 'ts-common/databaseConnection/neo4j';
+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';
+
+export const cypherQueryService = async (
+  db: DbConnection,
+  cypher: QueryText,
+  useCached: boolean,
+): Promise<GraphQueryResultMetaFromBackend> => {
+  let index = 0;
+  const disambiguatedQuery = cypher.query.replace(/\d{13}/g, () => (index++).toString());
+  const cacheKey = Bun.hash(JSON.stringify({ db: db, query: disambiguatedQuery })).toString();
+
+  if (QUERY_CACHE_DURATION === '') {
+    log.info('Query cache disabled, skipping cache check');
+  } else if (!useCached) {
+    log.info('Skipping cache check for query due to parameter', useCached);
+  } else {
+    const cachedMessage = await cacheCheck(cacheKey);
+    if (cachedMessage) {
+      log.debug('Cache hit for query', disambiguatedQuery);
+      return cachedMessage;
+    }
+  }
+
+  // TODO: only neo4j is supported for now
+  const connection = new Neo4jConnection(db);
+  try {
+    const [neo4jResult, neo4jCountResult] = await connection.run([cypher.query, cypher.countQuery]);
+    const graph = parseCypherQueryResult(neo4jResult.records);
+    const countGraph = parseCountCypherQueryResult(neo4jCountResult.records);
+
+    // calculate metadata
+    const result = graphQueryBackend2graphQuery(graph, countGraph);
+    result.nodeCounts.updatedAt = Date.now();
+
+    // cache result
+    const compressedMessage = Bun.gzipSync(JSON.stringify(result));
+    const base64Message = Buffer.from(compressedMessage).toString('base64');
+
+    if (QUERY_CACHE_DURATION !== '') {
+      // if cache enabled, cache the result
+      await redis.setWithExpire(cacheKey, base64Message, QUERY_CACHE_DURATION); // ttl in seconds
+    }
+
+    return result;
+  } catch (error) {
+    log.error('Error parsing query result:', cypher, error);
+    throw new Error('Error parsing query result');
+  } finally {
+    connection.close();
+  }
+};
diff --git a/src/readers/services/index.ts b/src/readers/services/index.ts
new file mode 100644
index 0000000..5a921cc
--- /dev/null
+++ b/src/readers/services/index.ts
@@ -0,0 +1,14 @@
+import type { DbConnection, GraphQueryResultMetaFromBackend } from 'ts-common';
+import type { QueryText } from '../../queryExecution/model';
+import { sqlQueryService } from './sqlService';
+import { cypherQueryService } from './cypherService';
+
+export const languageQueryService = (db: DbConnection, query: QueryText, useCached: boolean): Promise<GraphQueryResultMetaFromBackend> => {
+  if (db.type === 'postgres') {
+    return sqlQueryService(db, query, useCached);
+  } else if (db.type === 'neo4j' || db.type === 'memgraph') {
+    return cypherQueryService(db, query, useCached);
+  } else {
+    throw new Error('Unsupported database type');
+  }
+};
diff --git a/src/readers/services/sqlService.ts b/src/readers/services/sqlService.ts
new file mode 100644
index 0000000..fed1c52
--- /dev/null
+++ b/src/readers/services/sqlService.ts
@@ -0,0 +1,63 @@
+import { type DbConnection, type GraphQueryResultMetaFromBackend, graphQueryBackend2graphQuery } from 'ts-common';
+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,
+  queryText: QueryText,
+  useCached: boolean,
+): Promise<GraphQueryResultMetaFromBackend> => {
+  let index = 0;
+  const disambiguatedQuery = queryText.query.replace(/\d{13}/g, () => (index++).toString());
+  const cacheKey = Bun.hash(JSON.stringify({ db: db, query: disambiguatedQuery })).toString();
+
+  // if (QUERY_CACHE_DURATION === '') {
+  //   log.info('Query cache disabled, skipping cache check');
+  // } else if (!useCached) {
+  //   log.info('Skipping cache check for query due to parameter', useCached);
+  // } else {
+  //   const cachedMessage = await cacheCheck(cacheKey);
+  //   if (cachedMessage) {
+  //     log.debug('Cache hit for query', disambiguatedQuery);
+  //     return cachedMessage;
+  //   }
+  // }
+
+  const connection = new PgConnection(db);
+  try {
+    const [result, countResult] = await connection.run([queryText.query, queryText.countQuery]);
+    // console.log('result:', result);
+    // console.log('countResult:', countResult);
+
+    const graph = parseSQLQueryResult(result);
+    log.info('Parsed graph:', result);
+    const countGraph = parseCountSQLQueryResult(countResult);
+
+    // calculate metadata
+    const graphQueryResult = graphQueryBackend2graphQuery(graph, countGraph);
+    graphQueryResult.nodeCounts.updatedAt = Date.now();
+
+    // cache result
+    const compressedMessage = Bun.gzipSync(JSON.stringify(result));
+    const base64Message = Buffer.from(compressedMessage).toString('base64');
+
+    if (QUERY_CACHE_DURATION !== '') {
+      // if cache enabled, cache the result
+      await redis.setWithExpire(cacheKey, base64Message, QUERY_CACHE_DURATION); // ttl in seconds
+    }
+
+    // log.info('Query result:', graphQueryResult);
+
+    return graphQueryResult;
+  } catch (error) {
+    log.error('Error parsing query result:', queryText, error);
+    throw new Error('Error parsing query result');
+  } finally {
+    connection.close();
+  }
+};
-- 
GitLab