rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
278 lines • 14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SelectQueryParser = void 0;
const SelectQuery_1 = require("../models/SelectQuery");
const SelectClauseParser_1 = require("./SelectClauseParser");
const FromClauseParser_1 = require("./FromClauseParser");
const WhereClauseParser_1 = require("./WhereClauseParser");
const GroupByParser_1 = require("./GroupByParser");
const HavingParser_1 = require("./HavingParser");
const OrderByClauseParser_1 = require("./OrderByClauseParser");
const WindowClauseParser_1 = require("./WindowClauseParser");
const LimitClauseParser_1 = require("./LimitClauseParser");
const ForClauseParser_1 = require("./ForClauseParser");
const SqlTokenizer_1 = require("./SqlTokenizer");
const WithClauseParser_1 = require("./WithClauseParser");
const ValuesQueryParser_1 = require("./ValuesQueryParser");
const FetchClauseParser_1 = require("./FetchClauseParser");
const OffsetClauseParser_1 = require("./OffsetClauseParser");
class SelectQueryParser {
// Parse SQL string to AST (was: parse)
static parse(query) {
const tokenizer = new SqlTokenizer_1.SqlTokenizer(query);
const lexemes = tokenizer.readLexmes();
// Parse
const result = this.parseFromLexeme(lexemes, 0);
// Error if there are remaining tokens
if (result.newIndex < lexemes.length) {
throw new Error(`[SelectQueryParser] Syntax error: Unexpected token "${lexemes[result.newIndex].value}" at position ${result.newIndex}. The SELECT query is complete but there are additional tokens.`);
}
return result.value;
}
/**
* Analyzes SQL string for parsing without throwing errors.
* Returns a result object containing the parsed query on success,
* or error information if parsing fails.
*
* @param query SQL string to analyze
* @returns Analysis result containing query, error information, and success status
*/
/**
* Calculate character position from token index by finding token in original query
*/
static calculateCharacterPosition(query, lexemes, tokenIndex) {
var _a;
if (tokenIndex >= lexemes.length) {
return query.length;
}
// If lexeme has position information, use it
const lexeme = lexemes[tokenIndex];
if (((_a = lexeme.position) === null || _a === void 0 ? void 0 : _a.startPosition) !== undefined) {
return lexeme.position.startPosition;
}
// Fallback: search for token in original query
// Build search pattern from tokens up to the target
let searchStart = 0;
for (let i = 0; i < tokenIndex; i++) {
const tokenValue = lexemes[i].value;
const tokenPos = query.indexOf(tokenValue, searchStart);
if (tokenPos !== -1) {
searchStart = tokenPos + tokenValue.length;
}
}
const targetToken = lexemes[tokenIndex].value;
const tokenPos = query.indexOf(targetToken, searchStart);
return tokenPos !== -1 ? tokenPos : searchStart;
}
static analyze(query) {
let lexemes = [];
try {
const tokenizer = new SqlTokenizer_1.SqlTokenizer(query);
lexemes = tokenizer.readLexmes();
// Parse
const result = this.parseFromLexeme(lexemes, 0);
// Check for remaining tokens
if (result.newIndex < lexemes.length) {
const remainingTokens = lexemes.slice(result.newIndex).map(lex => lex.value);
const errorLexeme = lexemes[result.newIndex];
const errorPosition = this.calculateCharacterPosition(query, lexemes, result.newIndex);
return {
success: false,
query: result.value,
error: `Syntax error: Unexpected token "${errorLexeme.value}" at character position ${errorPosition}. The SELECT query is complete but there are additional tokens.`,
errorPosition: errorPosition,
remainingTokens: remainingTokens
};
}
return {
success: true,
query: result.value
};
}
catch (error) {
// Extract position information from error message if available
let errorPosition;
const errorMessage = error instanceof Error ? error.message : String(error);
// Try to extract token index from error message and convert to character position
const positionMatch = errorMessage.match(/position (\d+)/);
if (positionMatch) {
const tokenIndex = parseInt(positionMatch[1], 10);
errorPosition = this.calculateCharacterPosition(query, lexemes, tokenIndex);
}
return {
success: false,
error: errorMessage,
errorPosition: errorPosition
};
}
}
/**
* Asynchronously parse SQL string to AST.
* This method wraps the synchronous parse logic in a Promise for future extensibility.
* @param query SQL string to parse
* @returns Promise<SelectQuery>
*/
static async parseAsync(query) {
// For now, just wrap the sync parse in a resolved Promise
return Promise.resolve(this.parse(query));
}
// Parse from lexeme array (was: parse)
static parseFromLexeme(lexemes, index) {
let idx = index;
if (idx >= lexemes.length) {
throw new Error(`Syntax error: Unexpected end of input at position ${index}.`);
}
// Check if the first token is a SELECT keyword or VALUES
const firstToken = lexemes[idx].value;
if (!this.selectCommandSet.has(firstToken) && firstToken !== 'values') {
throw new Error(`Syntax error at position ${idx}: Expected 'SELECT' or 'VALUES' keyword but found "${lexemes[idx].value}".`);
}
let firstResult = this.selectCommandSet.has(firstToken)
? this.parseSimpleSelectQuery(lexemes, idx)
: this.parseValuesQuery(lexemes, idx);
let query = firstResult.value;
idx = firstResult.newIndex;
// check 'union'
while (idx < lexemes.length && this.unionCommandSet.has(lexemes[idx].value.toLowerCase())) {
const operator = lexemes[idx].value.toLowerCase();
idx++;
if (idx >= lexemes.length) {
throw new Error(`Syntax error at position ${idx}: Expected a query after '${operator.toUpperCase()}' but found end of input.`);
}
const nextToken = lexemes[idx].value.toLowerCase();
if (this.selectCommandSet.has(nextToken)) {
const result = this.parseSimpleSelectQuery(lexemes, idx);
query = new SelectQuery_1.BinarySelectQuery(query, operator, result.value);
idx = result.newIndex;
}
else if (nextToken === 'values') {
const result = this.parseValuesQuery(lexemes, idx);
query = new SelectQuery_1.BinarySelectQuery(query, operator, result.value);
idx = result.newIndex;
}
else {
throw new Error(`Syntax error at position ${idx}: Expected 'SELECT' or 'VALUES' after '${operator.toUpperCase()}' but found "${lexemes[idx].value}".`);
}
}
return { value: query, newIndex: idx };
}
static parseSimpleSelectQuery(lexemes, index) {
let idx = index;
let withClauseResult = null;
// Parse optional WITH clause
if (idx < lexemes.length && lexemes[idx].value === 'with') {
withClauseResult = WithClauseParser_1.WithClauseParser.parseFromLexeme(lexemes, idx);
idx = withClauseResult.newIndex;
}
// Capture comments from the current token (after WITH clause if present)
// This avoids duplication with WITH clause comments
const firstTokenComments = idx < lexemes.length ? lexemes[idx].comments : null;
// Parse SELECT clause (required)
if (idx >= lexemes.length || lexemes[idx].value !== 'select') {
throw new Error(`Syntax error at position ${idx}: Expected 'SELECT' keyword but found "${idx < lexemes.length ? lexemes[idx].value : 'end of input'}". SELECT queries must start with the SELECT keyword.`);
}
const selectClauseResult = SelectClauseParser_1.SelectClauseParser.parseFromLexeme(lexemes, idx);
idx = selectClauseResult.newIndex;
// If the select clause has the same comments as what we captured, clear them from the clause
// to avoid duplication (the query-level comments take precedence)
if (firstTokenComments && selectClauseResult.value.comments &&
JSON.stringify(firstTokenComments) === JSON.stringify(selectClauseResult.value.comments)) {
selectClauseResult.value.comments = null;
}
// Parse FROM clause (optional)
let fromClauseResult = null;
if (idx < lexemes.length && lexemes[idx].value === 'from') {
fromClauseResult = FromClauseParser_1.FromClauseParser.parseFromLexeme(lexemes, idx);
idx = fromClauseResult.newIndex;
}
// Parse WHERE clause (optional)
let whereClauseResult = null;
if (idx < lexemes.length && lexemes[idx].value === 'where') {
whereClauseResult = WhereClauseParser_1.WhereClauseParser.parseFromLexeme(lexemes, idx);
idx = whereClauseResult.newIndex;
}
// Parse GROUP BY clause (optional)
let groupByClauseResult = null;
if (idx < lexemes.length && lexemes[idx].value === 'group by') {
groupByClauseResult = GroupByParser_1.GroupByClauseParser.parseFromLexeme(lexemes, idx);
idx = groupByClauseResult.newIndex;
}
// Parse HAVING clause (optional)
let havingClauseResult = null;
if (idx < lexemes.length && lexemes[idx].value === 'having') {
havingClauseResult = HavingParser_1.HavingClauseParser.parseFromLexeme(lexemes, idx);
idx = havingClauseResult.newIndex;
}
// Parse WINDOW clause (optional)
let windowClauseResult = null;
if (idx < lexemes.length && lexemes[idx].value === 'window') {
windowClauseResult = WindowClauseParser_1.WindowClauseParser.parseFromLexeme(lexemes, idx);
idx = windowClauseResult.newIndex;
}
// Parse ORDER BY clause (optional)
let orderByClauseResult = null;
if (idx < lexemes.length && lexemes[idx].value === 'order by') {
orderByClauseResult = OrderByClauseParser_1.OrderByClauseParser.parseFromLexeme(lexemes, idx);
idx = orderByClauseResult.newIndex;
}
// Parse LIMIT clause (optional)
let limitClauseResult = null;
if (idx < lexemes.length && lexemes[idx].value === 'limit') {
limitClauseResult = LimitClauseParser_1.LimitClauseParser.parseFromLexeme(lexemes, idx);
idx = limitClauseResult.newIndex;
}
// Parse OFFSET clause (optional)
let offsetClauseResult = null;
if (idx < lexemes.length && lexemes[idx].value === 'offset') {
offsetClauseResult = OffsetClauseParser_1.OffsetClauseParser.parseFromLexeme(lexemes, idx);
idx = offsetClauseResult.newIndex;
}
// Parse FETCH clause (optional)
let fetchClauseResult = null;
if (idx < lexemes.length && lexemes[idx].value === 'fetch') {
fetchClauseResult = FetchClauseParser_1.FetchClauseParser.parseFromLexeme(lexemes, idx);
idx = fetchClauseResult.newIndex;
}
// Parse FOR clause (optional)
let forClauseResult = null;
if (idx < lexemes.length && lexemes[idx].value.toLowerCase() === 'for') {
forClauseResult = ForClauseParser_1.ForClauseParser.parseFromLexeme(lexemes, idx);
idx = forClauseResult.newIndex;
}
// Create and return the SelectQuery object
const selectQuery = new SelectQuery_1.SimpleSelectQuery({
withClause: withClauseResult ? withClauseResult.value : null,
selectClause: selectClauseResult.value,
fromClause: fromClauseResult ? fromClauseResult.value : null,
whereClause: whereClauseResult ? whereClauseResult.value : null,
groupByClause: groupByClauseResult ? groupByClauseResult.value : null,
havingClause: havingClauseResult ? havingClauseResult.value : null,
orderByClause: orderByClauseResult ? orderByClauseResult.value : null,
windowClause: windowClauseResult ? windowClauseResult.value : null,
limitClause: limitClauseResult ? limitClauseResult.value : null,
offsetClause: offsetClauseResult ? offsetClauseResult.value : null,
fetchClause: fetchClauseResult ? fetchClauseResult.value : null,
forClause: forClauseResult ? forClauseResult.value : null
});
// Set comments from the first token to the query object
selectQuery.comments = firstTokenComments;
return { value: selectQuery, newIndex: idx };
}
static parseValuesQuery(lexemes, index) {
// Use ValuesQueryParser to parse VALUES clause
const result = ValuesQueryParser_1.ValuesQueryParser.parseFromLexeme(lexemes, index);
// Return the result from ValuesQueryParser directly
return { value: result.value, newIndex: result.newIndex };
}
}
exports.SelectQueryParser = SelectQueryParser;
SelectQueryParser.unionCommandSet = new Set([
"union",
"union all",
"intersect",
"intersect all",
"except",
"except all",
]);
SelectQueryParser.selectCommandSet = new Set(["with", "select"]);
//# sourceMappingURL=SelectQueryParser.js.map