Custom intermediate format
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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<string> {
|
||||
visitSelect(ctx: SelectContext): string {
|
||||
export class ClickHouseQueryVisitor implements TSQLParserVisitor<QueryConfig> {
|
||||
visitSelect(ctx: SelectContext): QueryConfig {
|
||||
const selectSetStmt = ctx.selectSetStmt();
|
||||
if (selectSetStmt) {
|
||||
return this.visitSelectSetStmt(selectSetStmt);
|
||||
@@ -43,41 +45,55 @@ export class ClickHouseQueryVisitor implements TSQLParserVisitor<string> {
|
||||
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<string> {
|
||||
|
||||
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<string> {
|
||||
// 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: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user