rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
244 lines • 11.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SelectItemParser = exports.SelectClauseParser = void 0;
const Clause_1 = require("../models/Clause");
const Lexeme_1 = require("../models/Lexeme");
const ValueComponent_1 = require("../models/ValueComponent");
const SqlTokenizer_1 = require("./SqlTokenizer");
const ValueParser_1 = require("./ValueParser");
const HintClause_1 = require("../models/HintClause");
class SelectClauseParser {
// Parse SQL string to AST (was: parse)
static parse(query) {
const tokenizer = new SqlTokenizer_1.SqlTokenizer(query); // Initialize tokenizer
const lexemes = tokenizer.readLexmes(); // Get tokens
// Parse
const result = this.parseFromLexeme(lexemes, 0);
// Error if there are remaining tokens
if (result.newIndex < lexemes.length) {
throw new Error(`Syntax error: Unexpected token "${lexemes[result.newIndex].value}" at position ${result.newIndex}. The SELECT clause is complete but there are additional tokens.`);
}
return result.value;
}
// Parse from lexeme array (was: parse)
static parseFromLexeme(lexemes, index) {
let idx = index;
let distinct = null;
// Capture comments from the SELECT token
const selectTokenComments = idx < lexemes.length ? lexemes[idx].comments : null;
if (lexemes[idx].value !== 'select') {
throw new Error(`Syntax error at position ${idx}: Expected 'SELECT' keyword but found "${lexemes[idx].value}". SELECT clauses must start with the SELECT keyword.`);
}
idx++;
// Parse hint clauses (/*+ hint */) after SELECT
const hints = [];
while (idx < lexemes.length && HintClause_1.HintClause.isHintClause(lexemes[idx].value)) {
const hintContent = HintClause_1.HintClause.extractHintContent(lexemes[idx].value);
hints.push(new HintClause_1.HintClause(hintContent));
idx++;
}
if (idx < lexemes.length && lexemes[idx].value === 'distinct') {
idx++;
distinct = new Clause_1.Distinct();
}
else if (idx < lexemes.length && lexemes[idx].value === 'distinct on') {
idx++;
const argument = ValueParser_1.ValueParser.parseArgument(Lexeme_1.TokenType.OpenParen, Lexeme_1.TokenType.CloseParen, lexemes, idx);
distinct = new Clause_1.DistinctOn(argument.value);
idx = argument.newIndex;
}
const items = [];
const item = SelectItemParser.parseItem(lexemes, idx);
items.push(item.value);
idx = item.newIndex;
while (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Comma)) {
idx++;
const item = SelectItemParser.parseItem(lexemes, idx);
items.push(item.value);
idx = item.newIndex;
}
if (items.length === 0) {
throw new Error(`Syntax error at position ${index}: No select items found. The SELECT clause requires at least one expression to select.`);
}
else {
const clause = new Clause_1.SelectClause(items, distinct, hints);
return { value: clause, newIndex: idx };
}
}
}
exports.SelectClauseParser = SelectClauseParser;
// Extracted SelectItemParser for parsing individual select items
class SelectItemParser {
/**
* Parses a single select item from a SQL string.
* @param query The SQL string representing a select item (e.g. 'id as user_id').
* @returns The parsed SelectItem instance.
*/
static parse(query) {
const tokenizer = new SqlTokenizer_1.SqlTokenizer(query);
const lexemes = tokenizer.readLexmes();
const result = this.parseItem(lexemes, 0);
if (result.newIndex < lexemes.length) {
throw new Error(`Syntax error: Unexpected token "${lexemes[result.newIndex].value}" at position ${result.newIndex}. The select item is complete but there are additional tokens.`);
}
return result.value;
}
/**
* Parses a single select item from lexemes.
* @param lexemes The array of lexemes.
* @param index The starting index.
* @returns An object containing the SelectItem and the new index.
*/
static parseItem(lexemes, index) {
let idx = index;
// Extract positioned comments from the value token directly
const valueTokenComments = this.extractValueTokenComments(lexemes, idx);
const parsedValue = ValueParser_1.ValueParser.parseFromLexeme(lexemes, idx);
const value = parsedValue.value;
idx = parsedValue.newIndex;
// Parse optional AS keyword and extract comments directly
const { asComments, newIndex: asIndex } = this.parseAsKeyword(lexemes, idx);
idx = asIndex;
if (idx < lexemes.length && (lexemes[idx].type & Lexeme_1.TokenType.Identifier)) {
const alias = lexemes[idx].value;
const aliasComments = lexemes[idx].comments; // Capture comments from alias token
const aliasPositionedComments = lexemes[idx].positionedComments; // Capture positioned comments from alias token
idx++;
const selectItem = new Clause_1.SelectItem(value, alias);
// Apply all comments directly to selectItem (no collection then assignment)
this.applyValueTokenComments(selectItem, valueTokenComments);
this.applyAsKeywordComments(selectItem, asComments);
this.applyAliasComments(selectItem, aliasComments, aliasPositionedComments);
return {
value: selectItem,
newIndex: idx,
};
}
else if (value instanceof ValueComponent_1.ColumnReference && value.column.name !== "*") {
// nameless select item
const selectItem = new Clause_1.SelectItem(value, value.column.name);
// Apply value token and AS keyword comments directly
this.applyValueTokenComments(selectItem, valueTokenComments);
this.applyAsKeywordComments(selectItem, asComments);
return {
value: selectItem,
newIndex: idx,
};
}
// nameless select item
const selectItem = new Clause_1.SelectItem(value);
// Apply comments directly
this.applyValueTokenComments(selectItem, valueTokenComments);
this.applyAsKeywordComments(selectItem, asComments);
return {
value: selectItem,
newIndex: idx,
};
}
/**
* Recursively clear positioned comments from all nested components to prevent duplication
*/
static clearPositionedCommentsRecursively(component) {
if (!component || typeof component !== 'object') {
return;
}
// Clear positioned comments from this component
if ('positionedComments' in component) {
component.positionedComments = null;
}
// Recursively clear from common nested properties
if (component.left) {
this.clearPositionedCommentsRecursively(component.left);
}
if (component.right) {
this.clearPositionedCommentsRecursively(component.right);
}
if (component.qualifiedName) {
this.clearPositionedCommentsRecursively(component.qualifiedName);
}
if (component.table) {
this.clearPositionedCommentsRecursively(component.table);
}
if (component.name) {
this.clearPositionedCommentsRecursively(component.name);
}
if (component.args && Array.isArray(component.args)) {
component.args.forEach((arg) => {
this.clearPositionedCommentsRecursively(arg);
});
}
if (component.value) {
this.clearPositionedCommentsRecursively(component.value);
}
}
// Extract positioned comments from value token (no collection arrays)
static extractValueTokenComments(lexemes, index) {
if (index >= lexemes.length) {
return { positioned: null, legacy: null };
}
const token = lexemes[index];
return {
positioned: token.positionedComments && token.positionedComments.length > 0 ? token.positionedComments : null,
legacy: null // Value token legacy comments are not typically used
};
}
// Parse AS keyword and extract its comments directly
static parseAsKeyword(lexemes, index) {
if (index >= lexemes.length || lexemes[index].value !== 'as') {
return { asComments: { positioned: null, legacy: null }, newIndex: index };
}
const asToken = lexemes[index];
const asComments = {
positioned: asToken.positionedComments && asToken.positionedComments.length > 0 ? asToken.positionedComments : null,
legacy: asToken.comments && asToken.comments.length > 0 ? asToken.comments : null
};
return { asComments, newIndex: index + 1 };
}
// Apply value token comments directly to selectItem
static applyValueTokenComments(selectItem, valueTokenComments) {
if (valueTokenComments.positioned) {
for (const posComment of valueTokenComments.positioned) {
selectItem.addPositionedComments(posComment.position, posComment.comments);
}
this.clearValueTokenComments(selectItem);
}
}
// Apply AS keyword comments directly to selectItem
static applyAsKeywordComments(selectItem, asComments) {
if (asComments.positioned) {
selectItem.asKeywordPositionedComments = asComments.positioned;
}
else if (asComments.legacy) {
selectItem.asKeywordComments = asComments.legacy;
}
}
// Apply alias comments directly to selectItem
static applyAliasComments(selectItem, aliasComments, aliasPositionedComments) {
if (aliasPositionedComments && aliasPositionedComments.length > 0) {
selectItem.aliasPositionedComments = aliasPositionedComments;
}
else if (aliasComments && aliasComments.length > 0) {
selectItem.aliasComments = aliasComments;
}
}
// Clear positioned comments from value to avoid duplication
static clearValueTokenComments(selectItem) {
// Clear both positioned and legacy comments from the value to avoid duplication
if ('positionedComments' in selectItem.value) {
selectItem.value.positionedComments = null;
}
// Also clear positioned comments from nested IdentifierString (in QualifiedName)
if (selectItem.value instanceof ValueComponent_1.ColumnReference) {
const columnRef = selectItem.value;
if (columnRef.qualifiedName && columnRef.qualifiedName.name) {
columnRef.qualifiedName.name.positionedComments = null;
}
}
// Clear positioned comments from BinaryExpression children only to avoid duplication
if (selectItem.value instanceof ValueComponent_1.BinaryExpression) {
this.clearPositionedCommentsRecursively(selectItem.value);
}
}
}
exports.SelectItemParser = SelectItemParser;
//# sourceMappingURL=SelectClauseParser.js.map