From b50c6321da361bec0f9879fef63bcf822669a88f Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 10 Dec 2025 14:05:38 +0000 Subject: [PATCH] Custom intermediate format --- internal-packages/tsql/src/index.ts | 1 + .../tsql/src/query/ClickHouseQueryVisitor.ts | 388 ++++++++++++++---- .../tsql/src/query/QueryConfig.ts | 42 ++ internal-packages/tsql/src/query/TQuery.ts | 114 ++--- 4 files changed, 391 insertions(+), 154 deletions(-) create mode 100644 internal-packages/tsql/src/query/QueryConfig.ts diff --git a/internal-packages/tsql/src/index.ts b/internal-packages/tsql/src/index.ts index bcb0476b3..bc8c521db 100644 --- a/internal-packages/tsql/src/index.ts +++ b/internal-packages/tsql/src/index.ts @@ -2,3 +2,4 @@ import { Logger } from "@trigger.dev/core/logger"; export { TQuery } from "./query/TQuery.js"; export type { TQueryOptions } from "./query/TQuery.js"; +export type { QueryConfig } from "./query/QueryConfig.js"; diff --git a/internal-packages/tsql/src/query/ClickHouseQueryVisitor.ts b/internal-packages/tsql/src/query/ClickHouseQueryVisitor.ts index d4749315b..0cdf26748 100644 --- a/internal-packages/tsql/src/query/ClickHouseQueryVisitor.ts +++ b/internal-packages/tsql/src/query/ClickHouseQueryVisitor.ts @@ -27,12 +27,14 @@ import { WithClauseContext, } from "../grammar/TSQLParser.js"; import { TSQLParserVisitor } from "../grammar/TSQLParserVisitor.js"; +import { QueryConfig } from "./QueryConfig.js"; /** - * Visitor that converts TSQL AST to ClickHouse SQL + * Visitor that converts TSQL AST to a QueryConfig + * The QueryConfig can then be used to build a ClickhouseQueryBuilder */ -export class ClickHouseQueryVisitor implements TSQLParserVisitor { - visitSelect(ctx: SelectContext): string { +export class ClickHouseQueryVisitor implements TSQLParserVisitor { + visitSelect(ctx: SelectContext): QueryConfig { const selectSetStmt = ctx.selectSetStmt(); if (selectSetStmt) { return this.visitSelectSetStmt(selectSetStmt); @@ -43,41 +45,55 @@ export class ClickHouseQueryVisitor implements TSQLParserVisitor { return this.visitSelectStmt(selectStmt); } - // Handle tSQLxTagElement if needed - return ""; + // Handle tSQLxTagElement if needed - return empty config + return { + baseQuery: "", + whereClauses: [], + }; } - visitSelectSetStmt(ctx: SelectSetStmtContext): string { + visitSelectSetStmt(ctx: SelectSetStmtContext): QueryConfig { const selectStmtWithParens = ctx.selectStmtWithParens(); - let query = this.visitSelectStmtWithParens(selectStmtWithParens); + let config = this.visitSelectStmtWithParens(selectStmtWithParens); // Handle subsequent select set clauses (UNION, EXCEPT, INTERSECT) + // For now, we'll convert the config back to SQL for UNION operations + // This is a limitation - UNION queries can't use the query builder pattern easily const subsequentClauses = ctx.subsequentSelectSetClause(); - for (const clause of subsequentClauses) { - const op = clause.EXCEPT() - ? "EXCEPT" - : clause.UNION() - ? clause.ALL() - ? "UNION ALL" - : clause.DISTINCT() - ? "UNION DISTINCT" - : "UNION" - : clause.INTERSECT() - ? clause.DISTINCT() - ? "INTERSECT DISTINCT" - : "INTERSECT" - : ""; + if (subsequentClauses.length > 0) { + // Convert config to SQL for UNION operations + let query = this.configToSql(config); + for (const clause of subsequentClauses) { + const op = clause.EXCEPT() + ? "EXCEPT" + : clause.UNION() + ? clause.ALL() + ? "UNION ALL" + : clause.DISTINCT() + ? "UNION DISTINCT" + : "UNION" + : clause.INTERSECT() + ? clause.DISTINCT() + ? "INTERSECT DISTINCT" + : "INTERSECT" + : ""; - if (op) { - const nextSelect = clause.selectStmtWithParens(); - query += ` ${op} ${this.visitSelectStmtWithParens(nextSelect)}`; + if (op) { + const nextConfig = this.visitSelectStmtWithParens(clause.selectStmtWithParens()); + query += ` ${op} ${this.configToSql(nextConfig)}`; + } } + // Return as a base query (can't use query builder features with UNION) + return { + baseQuery: query, + whereClauses: [], + }; } - return query; + return config; } - visitSelectStmtWithParens(ctx: SelectStmtWithParensContext): string { + visitSelectStmtWithParens(ctx: SelectStmtWithParensContext): QueryConfig { const selectStmt = ctx.selectStmt(); if (selectStmt) { return this.visitSelectStmt(selectStmt); @@ -85,29 +101,47 @@ export class ClickHouseQueryVisitor implements TSQLParserVisitor { const selectSetStmt = ctx.selectSetStmt(); if (selectSetStmt) { - return `(${this.visitSelectSetStmt(selectSetStmt)})`; + const config = this.visitSelectSetStmt(selectSetStmt); + // Wrap in parentheses for subquery + return { + ...config, + baseQuery: `(${this.configToSql(config)})`, + whereClauses: [], // Subqueries don't contribute to outer WHERE + }; } // Handle placeholder if needed const placeholder = ctx.placeholder(); if (placeholder) { - return this.visitPlaceholder(placeholder); + const placeholderConfig = this.visitPlaceholder(placeholder); + return placeholderConfig; } - return this.getTextFromContext(ctx); + return { + baseQuery: this.getTextFromContext(ctx), + whereClauses: [], + }; } - visitPlaceholder(ctx: PlaceholderContext): string { - return this.getTextFromContext(ctx); + visitPlaceholder(ctx: PlaceholderContext): QueryConfig { + return { + baseQuery: this.getTextFromContext(ctx), + whereClauses: [], + }; } - visitSelectStmt(ctx: SelectStmtContext): string { - let parts: string[] = []; + visitSelectStmt(ctx: SelectStmtContext): QueryConfig { + const config: QueryConfig = { + baseQuery: "", + whereClauses: [], + }; + + const parts: string[] = []; // WITH clause const withClause = ctx.withClause(); if (withClause) { - parts.push(this.visitWithClause(withClause)); + parts.push(this.visitWithClauseString(withClause)); } // SELECT @@ -121,194 +155,382 @@ export class ClickHouseQueryVisitor implements TSQLParserVisitor { // TOP clause const topClause = ctx.topClause(); if (topClause) { - parts.push(this.visitTopClause(topClause)); + parts.push(this.visitTopClauseString(topClause)); } // Column list const columnExprList = ctx.columnExprList(); if (columnExprList) { - parts.push(this.visitColumnExprList(columnExprList)); + parts.push(this.visitColumnExprListString(columnExprList)); } // FROM clause const fromClause = ctx.fromClause(); if (fromClause) { - parts.push(this.visitFromClause(fromClause)); + parts.push(this.visitFromClauseString(fromClause)); } // Array JOIN const arrayJoinClause = ctx.arrayJoinClause(); if (arrayJoinClause) { - parts.push(this.visitArrayJoinClause(arrayJoinClause)); + parts.push(this.visitArrayJoinClauseString(arrayJoinClause)); } // PREWHERE const prewhereClause = ctx.prewhereClause(); if (prewhereClause) { - parts.push(this.visitPrewhereClause(prewhereClause)); + parts.push(this.visitPrewhereClauseString(prewhereClause)); } - // WHERE + // Base query is everything up to WHERE + config.baseQuery = parts.join(" "); + + // WHERE - extract to whereClauses array const whereClause = ctx.whereClause(); if (whereClause) { - parts.push(this.visitWhereClause(whereClause)); + const whereExpr = this.visitWhereClauseString(whereClause); + // Remove "WHERE " prefix + const whereCondition = whereExpr.replace(/^WHERE\s+/i, ""); + config.whereClauses.push({ + clause: whereCondition, + }); } // GROUP BY const groupByClause = ctx.groupByClause(); if (groupByClause) { - parts.push(this.visitGroupByClause(groupByClause)); + const groupByText = this.visitGroupByClauseString(groupByClause); + // Remove "GROUP BY " prefix + config.groupBy = groupByText.replace(/^GROUP\s+BY\s+/i, ""); } - // HAVING + // HAVING - add as WHERE clause (ClickHouse doesn't distinguish) const havingClause = ctx.havingClause(); if (havingClause) { - parts.push(this.visitHavingClause(havingClause)); + const havingText = this.visitHavingClauseString(havingClause); + // Remove "HAVING " prefix + const havingCondition = havingText.replace(/^HAVING\s+/i, ""); + config.whereClauses.push({ + clause: havingCondition, + }); } // WINDOW const windowClause = ctx.windowClause(); if (windowClause) { - parts.push(this.visitWindowClause(windowClause)); + const windowText = this.visitWindowClauseString(windowClause); + // Append to base query + config.baseQuery += " " + windowText; } // ORDER BY const orderByClause = ctx.orderByClause(); if (orderByClause) { - parts.push(this.visitOrderByClause(orderByClause)); + const orderByText = this.visitOrderByClauseString(orderByClause); + // Remove "ORDER BY " prefix + config.orderBy = orderByText.replace(/^ORDER\s+BY\s+/i, ""); } // LIMIT BY const limitByClause = ctx.limitByClause(); if (limitByClause) { - parts.push(this.visitLimitByClause(limitByClause)); + const limitByText = this.visitLimitByClauseString(limitByClause); + // Append to base query (LIMIT BY is not supported by query builder) + config.baseQuery += " " + limitByText; } // LIMIT / OFFSET const limitAndOffsetClause = ctx.limitAndOffsetClause(); if (limitAndOffsetClause) { - parts.push(this.visitLimitAndOffsetClause(limitAndOffsetClause)); + const limitText = this.visitLimitAndOffsetClauseString(limitAndOffsetClause); + // Try to extract limit number + const limitMatch = limitText.match(/LIMIT\s+(\d+)/i); + if (limitMatch) { + config.limit = parseInt(limitMatch[1], 10); + } else { + // If we can't parse it, append to base query + config.baseQuery += " " + limitText; + } } const offsetOnlyClause = ctx.offsetOnlyClause(); if (offsetOnlyClause) { - parts.push(this.visitOffsetOnlyClause(offsetOnlyClause)); + const offsetText = this.visitOffsetOnlyClauseString(offsetOnlyClause); + // Append to base query (OFFSET is not directly supported by query builder) + config.baseQuery += " " + offsetText; } // SETTINGS const settingsClause = ctx.settingsClause(); if (settingsClause) { - parts.push(this.visitSettingsClause(settingsClause)); + const settingsText = this.visitSettingsClauseString(settingsClause); + // Append to base query + config.baseQuery += " " + settingsText; } - return parts.join(" "); + return config; } - visitColumnExprList(ctx: ColumnExprListContext): string { + // Helper methods that return strings for building SQL fragments + // These are private and used internally, not part of the visitor interface + private visitColumnExprListString(ctx: ColumnExprListContext): string { const exprs: string[] = []; const columnExprs = ctx.columnExpr(); for (const expr of columnExprs) { - exprs.push(this.visitColumnExpr(expr)); + exprs.push(this.visitColumnExprString(expr)); } return exprs.join(", "); } - visitColumnExpr(ctx: ColumnExprContext): string { - // Use the text property which contains the original input text for this node + private visitColumnExprString(ctx: ColumnExprContext): string { return this.getTextFromContext(ctx); } - visitFromClause(ctx: FromClauseContext): string { + private visitFromClauseString(ctx: FromClauseContext): string { const joinExpr = ctx.joinExpr(); - return `FROM ${this.visitJoinExpr(joinExpr)}`; + return `FROM ${this.visitJoinExprString(joinExpr)}`; } - visitJoinExpr(ctx: JoinExprContext): string { + private visitJoinExprString(ctx: JoinExprContext): string { return this.getTextFromContext(ctx); } - visitWhereClause(ctx: WhereClauseContext): string { + private visitWhereClauseString(ctx: WhereClauseContext): string { const columnExpr = ctx.columnExpr(); - return `WHERE ${this.visitColumnExpr(columnExpr)}`; + return `WHERE ${this.visitColumnExprString(columnExpr)}`; } - visitGroupByClause(ctx: GroupByClauseContext): string { + private visitGroupByClauseString(ctx: GroupByClauseContext): string { return this.getTextFromContext(ctx); } - visitHavingClause(ctx: HavingClauseContext): string { + private visitHavingClauseString(ctx: HavingClauseContext): string { return this.getTextFromContext(ctx); } - visitOrderByClause(ctx: OrderByClauseContext): string { + private visitOrderByClauseString(ctx: OrderByClauseContext): string { return this.getTextFromContext(ctx); } - visitLimitByClause(ctx: LimitByClauseContext): string { + private visitLimitByClauseString(ctx: LimitByClauseContext): string { return this.getTextFromContext(ctx); } - visitLimitAndOffsetClause(ctx: LimitAndOffsetClauseContext): string { + private visitLimitAndOffsetClauseString(ctx: LimitAndOffsetClauseContext): string { return this.getTextFromContext(ctx); } - visitOffsetOnlyClause(ctx: OffsetOnlyClauseContext): string { + private visitOffsetOnlyClauseString(ctx: OffsetOnlyClauseContext): string { return this.getTextFromContext(ctx); } - visitSettingsClause(ctx: SettingsClauseContext): string { + private visitSettingsClauseString(ctx: SettingsClauseContext): string { return this.getTextFromContext(ctx); } - visitWithClause(ctx: WithClauseContext): string { + private visitWithClauseString(ctx: WithClauseContext): string { return this.getTextFromContext(ctx); } - visitTopClause(ctx: TopClauseContext): string { + private visitTopClauseString(ctx: TopClauseContext): string { return this.getTextFromContext(ctx); } - visitArrayJoinClause(ctx: ArrayJoinClauseContext): string { + private visitArrayJoinClauseString(ctx: ArrayJoinClauseContext): string { return this.getTextFromContext(ctx); } - visitPrewhereClause(ctx: PrewhereClauseContext): string { + private visitPrewhereClauseString(ctx: PrewhereClauseContext): string { return this.getTextFromContext(ctx); } - visitWindowClause(ctx: WindowClauseContext): string { + private visitWindowClauseString(ctx: WindowClauseContext): string { return this.getTextFromContext(ctx); } + // Interface methods that return QueryConfig (optional, so we can skip some) + visitColumnExprList(ctx: ColumnExprListContext): QueryConfig { + return { + baseQuery: this.visitColumnExprListString(ctx), + whereClauses: [], + }; + } + + visitColumnExpr(ctx: ColumnExprContext): QueryConfig { + return { + baseQuery: this.visitColumnExprString(ctx), + whereClauses: [], + }; + } + + visitFromClause(ctx: FromClauseContext): QueryConfig { + return { + baseQuery: this.visitFromClauseString(ctx), + whereClauses: [], + }; + } + + visitJoinExpr(ctx: JoinExprContext): QueryConfig { + return { + baseQuery: this.visitJoinExprString(ctx), + whereClauses: [], + }; + } + + visitWhereClause(ctx: WhereClauseContext): QueryConfig { + return { + baseQuery: this.visitWhereClauseString(ctx), + whereClauses: [], + }; + } + + visitGroupByClause(ctx: GroupByClauseContext): QueryConfig { + return { + baseQuery: this.visitGroupByClauseString(ctx), + whereClauses: [], + }; + } + + visitHavingClause(ctx: HavingClauseContext): QueryConfig { + return { + baseQuery: this.visitHavingClauseString(ctx), + whereClauses: [], + }; + } + + visitOrderByClause(ctx: OrderByClauseContext): QueryConfig { + return { + baseQuery: this.visitOrderByClauseString(ctx), + whereClauses: [], + }; + } + + visitLimitByClause(ctx: LimitByClauseContext): QueryConfig { + return { + baseQuery: this.visitLimitByClauseString(ctx), + whereClauses: [], + }; + } + + visitLimitAndOffsetClause(ctx: LimitAndOffsetClauseContext): QueryConfig { + return { + baseQuery: this.visitLimitAndOffsetClauseString(ctx), + whereClauses: [], + }; + } + + visitOffsetOnlyClause(ctx: OffsetOnlyClauseContext): QueryConfig { + return { + baseQuery: this.visitOffsetOnlyClauseString(ctx), + whereClauses: [], + }; + } + + visitSettingsClause(ctx: SettingsClauseContext): QueryConfig { + return { + baseQuery: this.visitSettingsClauseString(ctx), + whereClauses: [], + }; + } + + visitWithClause(ctx: WithClauseContext): QueryConfig { + return { + baseQuery: this.visitWithClauseString(ctx), + whereClauses: [], + }; + } + + visitTopClause(ctx: TopClauseContext): QueryConfig { + return { + baseQuery: this.visitTopClauseString(ctx), + whereClauses: [], + }; + } + + visitArrayJoinClause(ctx: ArrayJoinClauseContext): QueryConfig { + return { + baseQuery: this.visitArrayJoinClauseString(ctx), + whereClauses: [], + }; + } + + visitPrewhereClause(ctx: PrewhereClauseContext): QueryConfig { + return { + baseQuery: this.visitPrewhereClauseString(ctx), + whereClauses: [], + }; + } + + visitWindowClause(ctx: WindowClauseContext): QueryConfig { + return { + baseQuery: this.visitWindowClauseString(ctx), + whereClauses: [], + }; + } + + /** + * Convert a QueryConfig back to SQL string + * Used for UNION operations and subqueries + */ + private configToSql(config: QueryConfig): string { + let query = config.baseQuery; + if (config.whereClauses.length > 0) { + const clauses = config.whereClauses.map((w) => w.clause); + query += " WHERE " + clauses.join(" AND "); + } + if (config.groupBy) { + query += ` GROUP BY ${config.groupBy}`; + } + if (config.orderBy) { + query += ` ORDER BY ${config.orderBy}`; + } + if (config.limit !== undefined) { + query += ` LIMIT ${config.limit}`; + } + return query; + } + /** * Extract the original text from a parse tree context * This uses the text property which contains the original input text */ private getTextFromContext(ctx: ParserRuleContext): string { - // The text property exists at runtime but may not be in types return (ctx as any).text || ""; } // Required by ParseTreeVisitor interface - visit(tree: ParseTree): string { - // Use the text property which contains the original input text - return (tree as any).text || ""; + visit(tree: ParseTree): QueryConfig { + // For generic parse trees, return empty config + return { + baseQuery: (tree as any).text || "", + whereClauses: [], + }; } - visitChildren(node: ParserRuleContext): string { - // Visit all children and concatenate their results + visitChildren(node: ParserRuleContext): QueryConfig { + // Visit all children and combine their configs if (!node.children || node.children.length === 0) { return this.visit(node); } - return node.children.map((child: ParseTree) => this.visit(child)).join(" "); + // For now, just return the text representation + return { + baseQuery: (node as any).text || "", + whereClauses: [], + }; } // Visit terminal nodes - visitTerminal(node: TerminalNode): string { - return node.text; + visitTerminal(node: TerminalNode): QueryConfig { + return { + baseQuery: node.text, + whereClauses: [], + }; } - visitErrorNode(node: ErrorNode): string { - return ""; + visitErrorNode(node: ErrorNode): QueryConfig { + return { + baseQuery: "", + whereClauses: [], + }; } } diff --git a/internal-packages/tsql/src/query/QueryConfig.ts b/internal-packages/tsql/src/query/QueryConfig.ts new file mode 100644 index 000000000..1ba2fab75 --- /dev/null +++ b/internal-packages/tsql/src/query/QueryConfig.ts @@ -0,0 +1,42 @@ +import type { QueryParams } from "@internal/clickhouse/client/queryBuilder.js"; + +/** + * Configuration object that represents a parsed TSQL query + * This can be used to build a ClickhouseQueryBuilder + * The structure matches ClickhouseQueryBuilder's API + */ +export interface QueryConfig { + /** + * The base SELECT query without WHERE, GROUP BY, ORDER BY, LIMIT clauses + * Example: "SELECT id, name FROM users" + */ + baseQuery: string; + + /** + * WHERE clause conditions + * Each entry represents a call to queryBuilder.where(clause, params) + * The clauses will be joined with AND by the query builder + */ + whereClauses: Array<{ + clause: string; + params?: QueryParams; + }>; + + /** + * GROUP BY clause string + * Will be passed to queryBuilder.groupBy() + */ + groupBy?: string; + + /** + * ORDER BY clause string + * Will be passed to queryBuilder.orderBy() + */ + orderBy?: string; + + /** + * LIMIT value + * Will be passed to queryBuilder.limit() + */ + limit?: number; +} diff --git a/internal-packages/tsql/src/query/TQuery.ts b/internal-packages/tsql/src/query/TQuery.ts index e9900aca6..94411c90f 100644 --- a/internal-packages/tsql/src/query/TQuery.ts +++ b/internal-packages/tsql/src/query/TQuery.ts @@ -3,6 +3,7 @@ import { TSQLLexer } from "../grammar/TSQLLexer.js"; import { TSQLParser } from "../grammar/TSQLParser.js"; import { ClickHouseQueryVisitor } from "./ClickHouseQueryVisitor.js"; import type { ClickHouse } from "@internal/clickhouse"; +import { ClickhouseQueryBuilder } from "@internal/clickhouse/client/queryBuilder.js"; import { z } from "zod"; export interface TQueryOptions { @@ -43,80 +44,51 @@ export class TQuery { // Parse as a SELECT statement const tree = parser.select(); - // Convert AST to ClickHouse SQL + // Convert AST to QueryConfig const visitor = new ClickHouseQueryVisitor(); - let clickhouseQuery = visitor.visit(tree); + const queryConfig = visitor.visit(tree); - // Add WHERE clauses for scoping - clickhouseQuery = this.addScopingWhereClauses(clickhouseQuery); + // Use ClickhouseQueryBuilder to build the query + const queryBuilder = new ClickhouseQueryBuilder( + "tsql-query", + queryConfig.baseQuery, + this.clickhouseReader.reader, + schema + ); - // Execute the query using ClickHouse client - const queryFunction = this.clickhouseReader.reader.query({ - name: "tsql-query", - query: clickhouseQuery, - params: z.object({ - organizationId: z.string(), - projectId: z.string(), - environmentId: z.string(), - }), - schema, - }); - - return await queryFunction({ - organizationId: this.organizationId, - projectId: this.projectId, - environmentId: this.environmentId, - }); - } - - /** - * Add WHERE clauses for organization_id, project_id, and environment_id - * If the query already has a WHERE clause, we add AND conditions - */ - private addScopingWhereClauses(query: string): string { - const scopingConditions = [ - "organization_id = {organizationId: String}", - "project_id = {projectId: String}", - "environment_id = {environmentId: String}", - ].join(" AND "); - - const upperQuery = query.toUpperCase(); - - // Check if query already has a WHERE clause - const whereIndex = upperQuery.indexOf(" WHERE "); - if (whereIndex !== -1) { - // Find the end of the WHERE clause (before GROUP BY, HAVING, ORDER BY, LIMIT, etc.) - const groupByIndex = upperQuery.indexOf(" GROUP BY ", whereIndex); - const havingIndex = upperQuery.indexOf(" HAVING ", whereIndex); - const orderByIndex = upperQuery.indexOf(" ORDER BY ", whereIndex); - const limitIndex = upperQuery.indexOf(" LIMIT ", whereIndex); - - let whereEndIndex = query.length; - if (groupByIndex !== -1) whereEndIndex = Math.min(whereEndIndex, groupByIndex); - if (havingIndex !== -1) whereEndIndex = Math.min(whereEndIndex, havingIndex); - if (orderByIndex !== -1) whereEndIndex = Math.min(whereEndIndex, orderByIndex); - if (limitIndex !== -1) whereEndIndex = Math.min(whereEndIndex, limitIndex); - - // Insert AND conditions before the end of WHERE clause - const beforeWhereEnd = query.substring(0, whereEndIndex); - const afterWhereEnd = query.substring(whereEndIndex); - return `${beforeWhereEnd} AND ${scopingConditions}${afterWhereEnd}`; - } else { - // Add WHERE clause before GROUP BY, HAVING, ORDER BY, LIMIT, etc. - const groupByIndex = upperQuery.indexOf(" GROUP BY "); - const havingIndex = upperQuery.indexOf(" HAVING "); - const orderByIndex = upperQuery.indexOf(" ORDER BY "); - const limitIndex = upperQuery.indexOf(" LIMIT "); - - let insertIndex = query.length; - if (groupByIndex !== -1) insertIndex = Math.min(insertIndex, groupByIndex); - if (havingIndex !== -1) insertIndex = Math.min(insertIndex, havingIndex); - if (orderByIndex !== -1) insertIndex = Math.min(insertIndex, orderByIndex); - if (limitIndex !== -1) insertIndex = Math.min(insertIndex, limitIndex); - - return `${query.substring(0, insertIndex)} WHERE ${scopingConditions}${query.substring( - insertIndex - )}`; + // Add existing WHERE clauses from the TSQL query + for (const whereClause of queryConfig.whereClauses) { + queryBuilder.where(whereClause.clause, whereClause.params); } + + // Add scoping WHERE clauses + queryBuilder + .where("organization_id = {organizationId: String}", { + organizationId: this.organizationId, + }) + .where("project_id = {projectId: String}", { + projectId: this.projectId, + }) + .where("environment_id = {environmentId: String}", { + environmentId: this.environmentId, + }); + + // Add GROUP BY if present + if (queryConfig.groupBy) { + queryBuilder.groupBy(queryConfig.groupBy); + } + + // Add ORDER BY if present + if (queryConfig.orderBy) { + queryBuilder.orderBy(queryConfig.orderBy); + } + + // Add LIMIT if present + if (queryConfig.limit !== undefined) { + queryBuilder.limit(queryConfig.limit); + } + + // Execute the query + return await queryBuilder.execute(); } }