WIP fixing parser

This commit is contained in:
Matt Aitken
2025-12-11 09:45:53 +00:00
parent d6a3845f6a
commit fa7db2c965
4 changed files with 159 additions and 135 deletions
-3
View File
@@ -1,4 +1 @@
import { Logger } from "@trigger.dev/core/logger";
export { TQuery } from "./query/query.js";
export type { TQueryOptions } from "./query/query.js";
@@ -13,6 +13,7 @@ import type {
ArithmeticOperation,
Alias,
JoinExpr,
HogQLXTag,
} from "./ast.js";
import { ArithmeticOperationOp, CompareOperationOp } from "./ast.js";
import { SyntaxError } from "./errors.js";
@@ -32,10 +33,11 @@ function parseAndConvert(input: string) {
describe("TSQLParseTreeConverter", () => {
describe("SELECT statements", () => {
it("should convert a simple SELECT statement", () => {
it.only("should convert a simple SELECT statement", () => {
const ast = parseAndConvert("SELECT * FROM users");
expect(ast).toBeDefined();
console.log(ast);
expect("select" in ast).toBe(true);
const selectQuery = ast as SelectQuery;
expect(selectQuery.select).toBeDefined();
+75 -50
View File
@@ -110,7 +110,14 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
const end = stop !== undefined ? stop + 1 : undefined;
try {
const node = this.visitChildren(ctx);
if (node && typeof node === "object" && "start" in node && this.start !== undefined) {
// Only set position if node is a valid object and we have position info
if (
node &&
typeof node === "object" &&
node !== null &&
"start" in node &&
this.start !== undefined
) {
node.start = start;
node.end = end;
}
@@ -132,44 +139,14 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
/**
* Visit a parse tree node, dispatching to the appropriate visitor method.
* Uses type guards to safely handle ParseTree, ParserRuleContext, TerminalNode, and ErrorNode.
* Uses the accept method for proper double dispatch, which handles ErrorNode vs TerminalNode correctly.
*/
private visitParseTree(node: ParseTree): any {
// ErrorNode extends TerminalNode, so check ErrorNode first
if (this.isErrorNode(node)) {
return this.visitErrorNode(node);
}
if (this.isTerminalNode(node)) {
return this.visitTerminal(node);
}
if (this.isParserRuleContext(node)) {
return this.visit(node);
}
// Fallback: use accept method for double dispatch
// Use accept method for double dispatch - this will call the correct visit method
// based on the actual runtime type of the node
return node.accept(this);
}
/**
* Type guard to check if a ParseTree is an ErrorNode.
*/
private isErrorNode(node: ParseTree): node is ErrorNode {
return "symbol" in node && node.symbol !== undefined;
}
/**
* Type guard to check if a ParseTree is a TerminalNode.
*/
private isTerminalNode(node: ParseTree): node is TerminalNode {
return "symbol" in node && !("ruleIndex" in node);
}
/**
* Type guard to check if a ParseTree is a ParserRuleContext.
*/
private isParserRuleContext(node: ParseTree): node is ParserRuleContext {
return "ruleIndex" in node && "start" in node;
}
visitChildren(ctx: ParserRuleContext): any {
if (!ctx.children || ctx.children.length === 0) {
return null;
@@ -788,14 +765,16 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
} else {
throw new NotImplementedError(`Unsupported ColumnExprPrecedence1: ${ctx.getText()}`);
}
const left = this.visit(ctx.left);
const right = this.visit(ctx.right);
// Use columnExpr() method to get left and right operands
const left = this.visit(ctx.columnExpr(0));
const right = this.visit(ctx.columnExpr(1));
return { left, right, op };
}
visitColumnExprPrecedence2(ctx: any): ArithmeticOperation | Call {
const left = this.visit(ctx.left);
const right = this.visit(ctx.right);
// Use columnExpr() method to get left and right operands
const left = this.visit(ctx.columnExpr(0));
const right = this.visit(ctx.columnExpr(1));
if (ctx.PLUS()) {
return { left, right, op: ArithmeticOperationOp.Add };
@@ -822,8 +801,9 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
}
visitColumnExprPrecedence3(ctx: any): CompareOperation {
const left = this.visit(ctx.left);
const right = this.visit(ctx.right);
// Use columnExpr() method to get left and right operands
const left = this.visit(ctx.columnExpr(0));
const right = this.visit(ctx.columnExpr(1));
let op: CompareOperationOp;
if (ctx.EQ_SINGLE() || ctx.EQ_DOUBLE()) {
@@ -1140,12 +1120,15 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
visitTableIdentifier(ctx: any): string[] {
const nested = ctx.nestedIdentifier() ? this.visit(ctx.nestedIdentifier()) : [];
// Ensure nested is always an array
const nestedArray = Array.isArray(nested) ? nested : nested ? [nested] : [];
if (ctx.databaseIdentifier()) {
return [this.visit(ctx.databaseIdentifier()), ...nested];
const dbId = this.visit(ctx.databaseIdentifier());
return [dbId, ...nestedArray];
}
return nested;
return nestedArray;
}
visitTableArgList(ctx: any): Expr[] {
@@ -1157,6 +1140,12 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
}
visitNumberLiteral(ctx: any): Constant {
// NumberLiteralContext is a ParserRuleContext, use getText() to get the text
if (!ctx || typeof ctx.getText !== "function") {
throw new SyntaxError(
"Invalid number literal context - expected ParserRuleContext with getText()"
);
}
const text = ctx.getText().toLowerCase();
if (
text.includes(".") ||
@@ -1175,9 +1164,14 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
return { value: null };
}
if (ctx.STRING_LITERAL()) {
const text = parseStringLiteralCtx(ctx);
// STRING_LITERAL() returns a TerminalNode, which has getText()
const stringLiteral = ctx.STRING_LITERAL();
const text = parseStringLiteralCtx(stringLiteral);
return { value: text };
}
if (ctx.numberLiteral()) {
return this.visitNumberLiteral(ctx.numberLiteral());
}
return this.visitChildren(ctx);
}
@@ -1193,14 +1187,45 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
}
visitIdentifier(ctx: any): string {
let text = ctx.getText();
if (
text.length >= 2 &&
((text.startsWith("`") && text.endsWith("`")) || (text.startsWith('"') && text.endsWith('"')))
) {
text = parseStringLiteralText(text);
// IdentifierContext is a ParserRuleContext that has IDENTIFIER() method returning TerminalNode
// If ctx has IDENTIFIER() method, extract the terminal node
if (ctx.IDENTIFIER && typeof ctx.IDENTIFIER === "function") {
const terminalNode = ctx.IDENTIFIER();
if (terminalNode) {
// TerminalNode has getText() at runtime but types don't expose it
// Use symbol.text as fallback
let text = (terminalNode as any).getText?.() || terminalNode.symbol?.text || "";
if (
text.length >= 2 &&
((text.startsWith("`") && text.endsWith("`")) ||
(text.startsWith('"') && text.endsWith('"')))
) {
text = parseStringLiteralText(text);
}
return text;
}
}
return text;
// Fallback: if it's a ParserRuleContext, use getText()
if (typeof ctx.getText === "function") {
let text = ctx.getText();
if (
text.length >= 2 &&
((text.startsWith("`") && text.endsWith("`")) ||
(text.startsWith('"') && text.endsWith('"')))
) {
text = parseStringLiteralText(text);
}
return text;
}
// If it's already a string
if (typeof ctx === "string") {
return ctx;
}
// Try to get text from symbol if it's a TerminalNode
if (ctx.symbol && ctx.symbol.text) {
return ctx.symbol.text;
}
throw new SyntaxError("Invalid identifier context");
}
visitColumnExprNullish(ctx: any): Call {
+81 -81
View File
@@ -1,94 +1,94 @@
import { CharStreams, CommonTokenStream } from "antlr4ts";
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";
// import { CharStreams, CommonTokenStream } from "antlr4ts";
// import { TSQLLexer } from "../grammar/TSQLLexer.js";
// import { TSQLParser } from "../grammar/TSQLParser.js";
// import { ClickHouseQueryVisitor } from "./parser.js";
// import type { ClickHouse } from "@internal/clickhouse";
// import { ClickhouseQueryBuilder } from "@internal/clickhouse/client/queryBuilder.js";
// import { z } from "zod";
export interface TQueryOptions {
organizationId: string;
projectId: string;
environmentId: string;
}
// export interface TQueryOptions {
// organizationId: string;
// projectId: string;
// environmentId: string;
// }
export class TQuery {
private readonly organizationId: string;
private readonly projectId: string;
private readonly environmentId: string;
private readonly clickhouseReader: ClickHouse;
// export class TQuery {
// private readonly organizationId: string;
// private readonly projectId: string;
// private readonly environmentId: string;
// private readonly clickhouseReader: ClickHouse;
constructor(clickhouseReader: ClickHouse, options: TQueryOptions) {
this.clickhouseReader = clickhouseReader;
this.organizationId = options.organizationId;
this.projectId = options.projectId;
this.environmentId = options.environmentId;
}
// constructor(clickhouseReader: ClickHouse, options: TQueryOptions) {
// this.clickhouseReader = clickhouseReader;
// this.organizationId = options.organizationId;
// this.projectId = options.projectId;
// this.environmentId = options.environmentId;
// }
/**
* Execute a TSQL query and return the results
* @param input TSQL query string
* @param schema Zod schema for the output rows
* @returns Promise with query results
*/
async query<TOutput extends z.ZodSchema<any>>(
input: string,
schema: TOutput
): Promise<[Error | null, z.output<TOutput>[] | null]> {
// Parse the TSQL input
const inputStream = CharStreams.fromString(input);
const lexer = new TSQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer as any);
const parser = new TSQLParser(tokenStream);
// /**
// * Execute a TSQL query and return the results
// * @param input TSQL query string
// * @param schema Zod schema for the output rows
// * @returns Promise with query results
// */
// async query<TOutput extends z.ZodSchema<any>>(
// input: string,
// schema: TOutput
// ): Promise<[Error | null, z.output<TOutput>[] | null]> {
// // Parse the TSQL input
// const inputStream = CharStreams.fromString(input);
// const lexer = new TSQLLexer(inputStream);
// const tokenStream = new CommonTokenStream(lexer as any);
// const parser = new TSQLParser(tokenStream);
// Parse as a SELECT statement
const tree = parser.select();
// // Parse as a SELECT statement
// const tree = parser.select();
// Convert AST to QueryConfig
const visitor = new ClickHouseQueryVisitor();
const queryConfig = visitor.visit(tree);
// // Convert AST to QueryConfig
// const visitor = new ClickHouseQueryVisitor();
// const queryConfig = visitor.visit(tree);
// Use ClickhouseQueryBuilder to build the query
const queryBuilder = new ClickhouseQueryBuilder(
"tsql-query",
queryConfig.baseQuery,
this.clickhouseReader.reader,
schema
);
// // Use ClickhouseQueryBuilder to build the query
// const queryBuilder = new ClickhouseQueryBuilder(
// "tsql-query",
// queryConfig.baseQuery,
// this.clickhouseReader.reader,
// schema
// );
// Add existing WHERE clauses from the TSQL query
for (const whereClause of queryConfig.whereClauses) {
queryBuilder.where(whereClause.clause, whereClause.params);
}
// // 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 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 GROUP BY if present
// if (queryConfig.groupBy) {
// queryBuilder.groupBy(queryConfig.groupBy);
// }
// Add ORDER BY if present
if (queryConfig.orderBy) {
queryBuilder.orderBy(queryConfig.orderBy);
}
// // Add ORDER BY if present
// if (queryConfig.orderBy) {
// queryBuilder.orderBy(queryConfig.orderBy);
// }
// Add LIMIT if present
if (queryConfig.limit !== undefined) {
queryBuilder.limit(queryConfig.limit);
}
// // Add LIMIT if present
// if (queryConfig.limit !== undefined) {
// queryBuilder.limit(queryConfig.limit);
// }
// Execute the query
return await queryBuilder.execute();
}
}
// // Execute the query
// return await queryBuilder.execute();
// }
// }