import { Lexer } from './query-parser.lexer.ts'; import type { Token, TokenType } from './query-parser.types.ts'; import type { QueryConditionText, QueryConditionNumber, QueryFilter, QueryCondition } from '#root/utils/utils.query.ts'; class Parser { #tokens: Token[] = []; #position = 0; #current = (): Token => { return this.#tokens[this.#position]; }; #advance = (): Token => { const token = this.#current(); this.#position++; return token; }; #expect = (type: TokenType): Token => { const token = this.#current(); if (token.type !== type) { throw new Error(`Expected ${type} but got ${token.type} at position ${token.position}`); } return this.#advance(); }; #parseExpression = (): QueryFilter => { return this.#parseOr(); }; #parseOr = (): QueryFilter => { let left = this.#parseAnd(); while (this.#current().type === 'OR') { this.#advance(); const right = this.#parseAnd(); left = this.#combineWithOperator(left, right, 'or'); } return left; }; #parseAnd = (): QueryFilter => { let left = this.#parsePrimary(); while (this.#current().type === 'AND') { this.#advance(); const right = this.#parsePrimary(); left = this.#combineWithOperator(left, right, 'and'); } return left; }; #combineWithOperator = (left: QueryFilter, right: QueryFilter, operator: 'and' | 'or'): QueryFilter => { // If left is already an operator of the same type, add to its conditions if (left.type === 'operator' && left.operator === operator) { return { type: 'operator', operator, conditions: [...left.conditions, right], }; } return { type: 'operator', operator, conditions: [left, right], }; }; #parsePrimary = (): QueryFilter => { // Handle parenthesized expressions if (this.#current().type === 'LPAREN') { this.#advance(); const expr = this.#parseExpression(); this.#expect('RPAREN'); return expr; } // Must be a condition return this.#parseCondition(); }; #parseCondition = (): QueryCondition => { const field = this.#parseField(); const token = this.#current(); // IS NULL / IS NOT NULL if (token.type === 'IS') { this.#advance(); const isNot = this.#current().type === 'NOT'; if (isNot) { this.#advance(); } this.#expect('NULL'); // IS NULL / IS NOT NULL could be either text or number - default to text return { type: 'text', field, conditions: isNot ? { notEqual: undefined, equal: undefined } : { equal: null }, } satisfies QueryConditionText; } // NOT IN / NOT LIKE if (token.type === 'NOT') { this.#advance(); const nextToken = this.#current(); if (nextToken.type === 'IN') { this.#advance(); return this.#parseInCondition(field, true); } if (nextToken.type === 'LIKE') { this.#advance(); const pattern = this.#expect('STRING').value; return { type: 'text', field, conditions: { notLike: pattern }, }; } throw new Error(`Expected IN or LIKE after NOT at position ${nextToken.position}`); } // IN if (token.type === 'IN') { this.#advance(); return this.#parseInCondition(field, false); } // LIKE if (token.type === 'LIKE') { this.#advance(); const pattern = this.#expect('STRING').value; return { type: 'text', field, conditions: { like: pattern }, }; } // Comparison operators if (token.type === 'EQUALS') { this.#advance(); return this.#parseValueCondition(field, 'equals'); } if (token.type === 'NOT_EQUALS') { this.#advance(); return this.#parseValueCondition(field, 'notEquals'); } if (token.type === 'GREATER_THAN') { this.#advance(); const value = this.#parseNumber(); return { type: 'number', field, conditions: { greaterThan: value }, }; } if (token.type === 'GREATER_THAN_OR_EQUAL') { this.#advance(); const value = this.#parseNumber(); return { type: 'number', field, conditions: { greaterThanOrEqual: value }, }; } if (token.type === 'LESS_THAN') { this.#advance(); const value = this.#parseNumber(); return { type: 'number', field, conditions: { lessThan: value }, }; } if (token.type === 'LESS_THAN_OR_EQUAL') { this.#advance(); const value = this.#parseNumber(); return { type: 'number', field, conditions: { lessThanOrEqual: value }, }; } throw new Error(`Unexpected token '${token.value}' at position ${token.position}`); }; #parseField = (): string[] => { const parts: string[] = []; parts.push(this.#expect('IDENTIFIER').value); while (this.#current().type === 'DOT') { this.#advance(); parts.push(this.#expect('IDENTIFIER').value); } return parts; }; #parseValueCondition = (field: string[], operator: 'equals' | 'notEquals'): QueryCondition => { const token = this.#current(); if (token.type === 'STRING') { this.#advance(); const textCondition: QueryConditionText = { type: 'text', field, conditions: operator === 'equals' ? { equal: token.value } : { notEqual: token.value }, }; return textCondition; } if (token.type === 'NUMBER') { this.#advance(); const value = parseFloat(token.value); const numCondition: QueryConditionNumber = { type: 'number', field, conditions: operator === 'equals' ? { equals: value } : { notEquals: value }, }; return numCondition; } if (token.type === 'NULL') { this.#advance(); // NULL equality - default to text type return { type: 'text', field, conditions: operator === 'equals' ? { equal: null } : {}, } as QueryConditionText; } throw new Error(`Expected value but got ${token.type} at position ${token.position}`); }; #parseNumber = (): number => { const token = this.#expect('NUMBER'); return parseFloat(token.value); }; #parseInCondition = (field: string[], isNot: boolean): QueryCondition => { this.#expect('LPAREN'); const firstToken = this.#current(); if (firstToken.type === 'STRING') { // Text IN const values: string[] = []; values.push(this.#advance().value); while (this.#current().type === 'COMMA') { this.#advance(); values.push(this.#expect('STRING').value); } this.#expect('RPAREN'); return { type: 'text', field, conditions: isNot ? { notIn: values } : { in: values }, }; } if (firstToken.type === 'NUMBER') { // Numeric IN const values: number[] = []; values.push(parseFloat(this.#advance().value)); while (this.#current().type === 'COMMA') { this.#advance(); values.push(parseFloat(this.#expect('NUMBER').value)); } this.#expect('RPAREN'); return { type: 'number', field, conditions: isNot ? { notIn: values } : { in: values }, }; } throw new Error(`Expected STRING or NUMBER in IN list at position ${firstToken.position}`); }; public parse(input: string): QueryFilter { const lexer = new Lexer(input); this.#tokens = lexer.tokenize(); this.#position = 0; const result = this.#parseExpression(); if (this.#current().type !== 'EOF') { throw new Error(`Unexpected token '${this.#current().value}' at position ${this.#current().position}`); } return result; } } export { Parser };