chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1,288 @@
lexer grammar TSQLLexer;
// NB! We cat TSQLLexter.typescript.g4 when generating the grammar.
// NOTE: don't forget to add new keywords to the parser rule "keyword"!
// Keywords
ALL: A L L;
AND: A N D;
ANTI: A N T I;
ANY: A N Y;
ARRAY: A R R A Y;
AS: A S;
ASCENDING: A S C | A S C E N D I N G;
ASOF: A S O F;
BETWEEN: B E T W E E N;
BOTH: B O T H;
BY: B Y;
CASE: C A S E;
CAST: C A S T;
CATCH: C A T C H;
COHORT: C O H O R T;
COLLATE: C O L L A T E;
CROSS: C R O S S;
CUBE: C U B E;
CURRENT: C U R R E N T;
DATE: D A T E;
DAY: D A Y;
DESC: D E S C;
DESCENDING: D E S C E N D I N G;
DISTINCT: D I S T I N C T;
ELSE: E L S E;
END: E N D;
EXCEPT: E X C E P T;
EXTRACT: E X T R A C T;
FINAL: F I N A L;
FINALLY: F I N A L L Y;
FIRST: F I R S T;
FN: F N;
FOLLOWING: F O L L O W I N G;
FOR: F O R;
FROM: F R O M;
FULL: F U L L;
FUN: F U N;
GROUP: G R O U P;
HAVING: H A V I N G;
HOUR: H O U R;
ID: I D;
IF: I F;
ILIKE: I L I K E;
IN: I N;
INF: I N F | I N F I N I T Y;
INNER: I N N E R;
INTERSECT: I N T E R S E C T;
INTERVAL: I N T E R V A L;
IS: I S;
JOIN: J O I N;
KEY: K E Y;
LAST: L A S T;
LEADING: L E A D I N G;
LEFT: L E F T;
LET: L E T;
LIKE: L I K E;
LIMIT: L I M I T;
MINUTE: M I N U T E;
MONTH: M O N T H;
NAN_SQL: N A N; // conflicts with macro NAN
NOT: N O T;
NULL_SQL: N U L L; // conflicts with macro NULL
NULLS: N U L L S;
OFFSET: O F F S E T;
ON: O N;
OR: O R;
ORDER: O R D E R;
OUTER: O U T E R;
OVER: O V E R;
PARTITION: P A R T I T I O N;
PRECEDING: P R E C E D I N G;
PREWHERE: P R E W H E R E;
QUARTER: Q U A R T E R;
RANGE: R A N G E;
RETURN: R E T U R N;
RIGHT: R I G H T;
ROLLUP: R O L L U P;
ROW: R O W;
ROWS: R O W S;
SAMPLE: S A M P L E;
SECOND: S E C O N D;
SELECT: S E L E C T;
SEMI: S E M I;
SETTINGS: S E T T I N G S;
SUBSTRING: S U B S T R I N G;
THEN: T H E N;
THROW: T H R O W;
TIES: T I E S;
TIMESTAMP: T I M E S T A M P;
TO: T O;
TOP: T O P;
TOTALS: T O T A L S;
TRAILING: T R A I L I N G;
TRIM: T R I M;
TRUNCATE: T R U N C A T E;
TRY: T R Y;
UNBOUNDED: U N B O U N D E D;
UNION: U N I O N;
USING: U S I N G;
WEEK: W E E K;
WHEN: W H E N;
WHERE: W H E R E;
WHILE: W H I L E;
WINDOW: W I N D O W;
WITH: W I T H;
YEAR: Y E A R | Y Y Y Y;
// Tokens
// copied from clickhouse_driver/util/escape.py
ESCAPE_CHAR_COMMON
: BACKSLASH B
| BACKSLASH F
| BACKSLASH R
| BACKSLASH N
| BACKSLASH T
| BACKSLASH '0'
| BACKSLASH A
| BACKSLASH V
| BACKSLASH BACKSLASH;
IDENTIFIER
: (LETTER | UNDERSCORE | DOLLAR) (LETTER | UNDERSCORE | DEC_DIGIT | DOLLAR)*
| BACKQUOTE ( ~([\\`]) | ESCAPE_CHAR_COMMON | BACKSLASH QUOTE_SINGLE | (BACKQUOTE BACKQUOTE) )* BACKQUOTE
| QUOTE_DOUBLE ( ~([\\"]) | ESCAPE_CHAR_COMMON | BACKSLASH QUOTE_DOUBLE | (QUOTE_DOUBLE QUOTE_DOUBLE) )* QUOTE_DOUBLE
;
FLOATING_LITERAL
: HEXADECIMAL_LITERAL DOT HEX_DIGIT* (P | E) (PLUS | DASH)? DEC_DIGIT+
| HEXADECIMAL_LITERAL (P | E) (PLUS | DASH)? DEC_DIGIT+
| DECIMAL_LITERAL DOT DEC_DIGIT* E (PLUS | DASH)? DEC_DIGIT+
| DOT DECIMAL_LITERAL E (PLUS | DASH)? DEC_DIGIT+
| DECIMAL_LITERAL E (PLUS | DASH)? DEC_DIGIT+
;
OCTAL_LITERAL: '0' OCT_DIGIT+;
DECIMAL_LITERAL: DEC_DIGIT+;
HEXADECIMAL_LITERAL: '0' X HEX_DIGIT+;
// It's important that quote-symbol is a single character.
STRING_LITERAL: QUOTE_SINGLE ( ~([\\']) | ESCAPE_CHAR_COMMON | BACKSLASH QUOTE_SINGLE | (QUOTE_SINGLE QUOTE_SINGLE) )* QUOTE_SINGLE;
// Alphabet and allowed symbols
fragment A: [aA];
fragment B: [bB];
fragment C: [cC];
fragment D: [dD];
fragment E: [eE];
fragment F: [fF];
fragment G: [gG];
fragment H: [hH];
fragment I: [iI];
fragment J: [jJ];
fragment K: [kK];
fragment L: [lL];
fragment M: [mM];
fragment N: [nN];
fragment O: [oO];
fragment P: [pP];
fragment Q: [qQ];
fragment R: [rR];
fragment S: [sS];
fragment T: [tT];
fragment U: [uU];
fragment V: [vV];
fragment W: [wW];
fragment X: [xX];
fragment Y: [yY];
fragment Z: [zZ];
fragment LETTER: [a-zA-Z];
fragment OCT_DIGIT: [0-7];
fragment DEC_DIGIT: [0-9];
fragment HEX_DIGIT: [0-9a-fA-F];
ARROW: '->';
ASTERISK: '*';
BACKQUOTE: '`';
BACKSLASH: '\\';
COLON: ':';
COMMA: ',';
CONCAT: '||';
DASH: '-';
DOLLAR: '$';
DOT: '.';
EQ_DOUBLE: '==';
EQ_SINGLE: '=';
GT_EQ: '>=';
GT: '>';
HASH: '#';
IREGEX_SINGLE: '~*';
IREGEX_DOUBLE: '=~*';
LBRACE: '{' -> pushMode(DEFAULT_MODE);
LBRACKET: '[';
LPAREN: '(';
LT_EQ: '<=';
TAG_LT_SLASH: '</' -> type(LT_SLASH), pushMode(TSQLX_TAG_CLOSE);
TAG_LT_OPEN: '<' {isOpeningTag()}? -> type(LT), pushMode(TSQLX_TAG_OPEN);
LT: '<';
LT_SLASH: '</';
NOT_EQ: '!=' | '<>';
NOT_IREGEX: '!~*';
NOT_REGEX: '!~';
NULL_PROPERTY: '?.';
NULLISH: '??';
PERCENT: '%';
PLUS: '+';
QUERY: '?';
QUOTE_DOUBLE: '"';
QUOTE_SINGLE_TEMPLATE: 'f\'' -> pushMode(IN_TEMPLATE_STRING); // start of regular f'' template strings
QUOTE_SINGLE_TEMPLATE_FULL: 'F\'' -> pushMode(IN_FULL_TEMPLATE_STRING); // magic F' symbol used to parse "full text" templates
QUOTE_SINGLE: '\'';
REGEX_SINGLE: '~';
REGEX_DOUBLE: '=~';
RBRACE: '}' -> popMode;
RBRACKET: ']';
RPAREN: ')';
SEMICOLON: ';';
SLASH: '/';
SLASH_GT: '/>';
UNDERSCORE: '_';
// Comments and whitespace
MULTI_LINE_COMMENT: '/*' .*? '*/' -> skip;
SINGLE_LINE_COMMENT: ('--' | '//') ~('\n'|'\r')* ('\n' | '\r' | EOF) -> skip;
// whitespace is hidden and not skipped so that it's preserved in ANTLR errors like "no viable alternative"
WHITESPACE: [ \u000B\u000C\t\r\n] -> channel(HIDDEN);
// ───────── f' TEMPLATE STRING MODE ─────────
mode IN_TEMPLATE_STRING;
STRING_TEXT: ((~([\\'{])) | ESCAPE_CHAR_COMMON | BACKSLASH QUOTE_SINGLE | (BACKSLASH LBRACE) | (QUOTE_SINGLE QUOTE_SINGLE))+;
STRING_ESCAPE_TRIGGER: LBRACE -> pushMode(DEFAULT_MODE);
STRING_QUOTE_SINGLE: QUOTE_SINGLE -> type(QUOTE_SINGLE), popMode;
// ───────── F' FULL TEMPLATE STRING MODE ─────────
// a magic F' takes us to "full template strings" mode, where we don't need to escape single quotes and parse until EOF
// this can't be used within a normal columnExpr, but has to be parsed for separately
mode IN_FULL_TEMPLATE_STRING;
FULL_STRING_TEXT: ((~([{])) | ESCAPE_CHAR_COMMON | (BACKSLASH LBRACE))+;
FULL_STRING_ESCAPE_TRIGGER: LBRACE -> pushMode(DEFAULT_MODE);
// ───────── TSQLX TAG MODE for opening/self-closing tags ─────────
mode TSQLX_TAG_OPEN;
TAG_SELF_CLOSE_GT : '/>' -> type(SLASH_GT), popMode; // <tag …/>
TAG_OPEN_GT : '>' -> type(GT), popMode, pushMode(TSQLX_TEXT); // <tag …>
// minimal token set; map everything back to the default token types
TAG_IDENT : [a-zA-Z_][a-zA-Z0-9_-]* -> type(IDENTIFIER);
TAG_EQ : '=' -> type(EQ_SINGLE);
TAG_STRING : STRING_LITERAL -> type(STRING_LITERAL);
TAG_WS : [ \t\r\n]+ -> channel(HIDDEN);
TAG_LBRACE : '{' -> type(LBRACE), pushMode(DEFAULT_MODE);
// ───────── TSQLX TAG MODE for closing tags ─────────
mode TSQLX_TAG_CLOSE;
TAGC_GT : '>' -> type(GT), popMode; // *** no TEXT push ***
TAGC_IDENT : [a-zA-Z_][a-zA-Z0-9_-]* -> type(IDENTIFIER);
TAGC_WS : [ \t\r\n]+ -> channel(HIDDEN);
// ───────── TSQLX TEXT MODE ─────────
mode TSQLX_TEXT;
TSQLX_TEXT_TEXT
: ~[<{]+ ; // everything except “{” or “<”
TSQLX_TEXT_LBRACE
: '{' -> type(LBRACE), pushMode(DEFAULT_MODE);
TSQLX_TEXT_LT_SLASH
: '</' -> type(LT_SLASH), popMode, pushMode(TSQLX_TAG_CLOSE);
TSQLX_TEXT_LT
: '<' -> type(LT), pushMode(TSQLX_TAG_OPEN);
TSQLX_TEXT_WS
: [ \t\r\n]+ -> channel(HIDDEN);
File diff suppressed because one or more lines are too long
@@ -0,0 +1,208 @@
ALL=1
AND=2
ANTI=3
ANY=4
ARRAY=5
AS=6
ASCENDING=7
ASOF=8
BETWEEN=9
BOTH=10
BY=11
CASE=12
CAST=13
CATCH=14
COHORT=15
COLLATE=16
CROSS=17
CUBE=18
CURRENT=19
DATE=20
DAY=21
DESC=22
DESCENDING=23
DISTINCT=24
ELSE=25
END=26
EXCEPT=27
EXTRACT=28
FINAL=29
FINALLY=30
FIRST=31
FN=32
FOLLOWING=33
FOR=34
FROM=35
FULL=36
FUN=37
GROUP=38
HAVING=39
HOUR=40
ID=41
IF=42
ILIKE=43
IN=44
INF=45
INNER=46
INTERSECT=47
INTERVAL=48
IS=49
JOIN=50
KEY=51
LAST=52
LEADING=53
LEFT=54
LET=55
LIKE=56
LIMIT=57
MINUTE=58
MONTH=59
NAN_SQL=60
NOT=61
NULL_SQL=62
NULLS=63
OFFSET=64
ON=65
OR=66
ORDER=67
OUTER=68
OVER=69
PARTITION=70
PRECEDING=71
PREWHERE=72
QUARTER=73
RANGE=74
RETURN=75
RIGHT=76
ROLLUP=77
ROW=78
ROWS=79
SAMPLE=80
SECOND=81
SELECT=82
SEMI=83
SETTINGS=84
SUBSTRING=85
THEN=86
THROW=87
TIES=88
TIMESTAMP=89
TO=90
TOP=91
TOTALS=92
TRAILING=93
TRIM=94
TRUNCATE=95
TRY=96
UNBOUNDED=97
UNION=98
USING=99
WEEK=100
WHEN=101
WHERE=102
WHILE=103
WINDOW=104
WITH=105
YEAR=106
ESCAPE_CHAR_COMMON=107
IDENTIFIER=108
FLOATING_LITERAL=109
OCTAL_LITERAL=110
DECIMAL_LITERAL=111
HEXADECIMAL_LITERAL=112
STRING_LITERAL=113
ARROW=114
ASTERISK=115
BACKQUOTE=116
BACKSLASH=117
COLON=118
COMMA=119
CONCAT=120
DASH=121
DOLLAR=122
DOT=123
EQ_DOUBLE=124
EQ_SINGLE=125
GT_EQ=126
GT=127
HASH=128
IREGEX_SINGLE=129
IREGEX_DOUBLE=130
LBRACE=131
LBRACKET=132
LPAREN=133
LT_EQ=134
LT=135
LT_SLASH=136
NOT_EQ=137
NOT_IREGEX=138
NOT_REGEX=139
NULL_PROPERTY=140
NULLISH=141
PERCENT=142
PLUS=143
QUERY=144
QUOTE_DOUBLE=145
QUOTE_SINGLE_TEMPLATE=146
QUOTE_SINGLE_TEMPLATE_FULL=147
QUOTE_SINGLE=148
REGEX_SINGLE=149
REGEX_DOUBLE=150
RBRACE=151
RBRACKET=152
RPAREN=153
SEMICOLON=154
SLASH=155
SLASH_GT=156
UNDERSCORE=157
MULTI_LINE_COMMENT=158
SINGLE_LINE_COMMENT=159
WHITESPACE=160
STRING_TEXT=161
STRING_ESCAPE_TRIGGER=162
FULL_STRING_TEXT=163
FULL_STRING_ESCAPE_TRIGGER=164
TAG_WS=165
TAGC_WS=166
TSQLX_TEXT_TEXT=167
TSQLX_TEXT_WS=168
'->'=114
'*'=115
'`'=116
'\\'=117
':'=118
','=119
'||'=120
'-'=121
'$'=122
'.'=123
'=='=124
'>='=126
'#'=128
'~*'=129
'=~*'=130
'{'=131
'['=132
'('=133
'<='=134
'<'=135
'</'=136
'!~*'=138
'!~'=139
'?.'=140
'??'=141
'%'=142
'+'=143
'?'=144
'"'=145
'f\''=146
'F\''=147
'\''=148
'~'=149
'=~'=150
'}'=151
']'=152
')'=153
';'=154
'/'=155
'_'=157
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,81 @@
lexer grammar TSQLLexer;
@header {
// put any global imports you need here
}
@members {
private _peekChar(k: number): string {
// Return the k-th look-ahead as a *single-char string* or '\0' at EOF.
const c = this._input.LA(k); // int code point or IntStream.EOF (-1)
if (c < 0 || c > 0x10FFFF) { // EOF or out-of-range → sentinel
return '\0';
}
return String.fromCharCode(c);
}
private _skipWsAndComments(idx: number): number {
// Return the first index ≥ idx that is *not* whitespace / single-line comment.
while (true) {
const ch = this._peekChar(idx);
if (/\s/.test(ch)) { // spaces, newlines, tabs …
idx++;
continue;
}
// single-line comments
if (ch === '/' && this._peekChar(idx + 1) === '/') { // //
idx += 2;
} else if (ch === '-' && this._peekChar(idx + 1) === '-') { // --
idx += 2;
} else if (ch === '#') { // #
idx++;
} else {
break; // no ws / comment
}
// consume until EOL / EOF
while (!['\0', '\n', '\r'].includes(this._peekChar(idx))) {
idx++;
}
}
return idx;
}
// ───── opening tag test ─────
isOpeningTag(): boolean {
const ch1 = this._peekChar(1);
if (!(/[a-zA-Z]/.test(ch1) || ch1 === '_')) {
return false; // not a tag name start
}
// skip tag name
let i = 2;
while (true) {
const ch = this._peekChar(i);
if (/[a-zA-Z0-9]/.test(ch) || ch === '_' || ch === '-') {
i++;
} else {
break;
}
}
let ch = this._peekChar(i);
// immediate delimiter → tag
if (ch === '>' || ch === '/') {
return true;
}
// need to look beyond whitespace
if (/\s/.test(ch)) {
i = this._skipWsAndComments(i + 1);
ch = this._peekChar(i);
return ch === '>' || ch === '/' || /[a-zA-Z0-9]/.test(ch) || ch === '_';
}
// anything else → not a tag
return false;
}
}
@@ -0,0 +1,315 @@
parser grammar TSQLParser;
options {
tokenVocab = TSQLLexer;
}
program: declaration* EOF;
declaration: varDecl | statement ;
expression: columnExpr;
varDecl: LET identifier ( COLON EQ_SINGLE expression )? ;
identifierList: identifier (COMMA identifier)* COMMA?;
statement : returnStmt
| throwStmt
| tryCatchStmt
| ifStmt
| whileStmt
| forInStmt
| forStmt
| funcStmt
| varAssignment
| block
| exprStmt
| emptyStmt
;
returnStmt : RETURN expression? SEMICOLON?;
throwStmt : THROW expression? SEMICOLON?;
catchBlock : CATCH (LPAREN catchVar=identifier (COLON catchType=identifier)? RPAREN)? catchStmt=block;
tryCatchStmt : TRY tryStmt=block catchBlock* (FINALLY finallyStmt=block)?;
ifStmt : IF LPAREN expression RPAREN statement ( ELSE statement )? ;
whileStmt : WHILE LPAREN expression RPAREN statement SEMICOLON?;
forStmt : FOR LPAREN
(initializerVarDeclr=varDecl | initializerVarAssignment=varAssignment | initializerExpression=expression)? SEMICOLON
condition=expression? SEMICOLON
(incrementVarDeclr=varDecl | incrementVarAssignment=varAssignment | incrementExpression=expression)?
RPAREN statement SEMICOLON?;
forInStmt : FOR LPAREN LET identifier (COMMA identifier)? IN expression RPAREN statement SEMICOLON?;
funcStmt : (FN | FUN) identifier LPAREN identifierList? RPAREN block;
varAssignment : expression COLON EQ_SINGLE expression ;
exprStmt : expression SEMICOLON?;
emptyStmt : SEMICOLON ;
block : LBRACE declaration* RBRACE ;
kvPair: expression ':' expression ;
kvPairList: kvPair (COMMA kvPair)* COMMA?;
// SELECT statement
select: (selectSetStmt | selectStmt | tSQLxTagElement) SEMICOLON? EOF;
selectStmtWithParens: selectStmt | LPAREN selectSetStmt RPAREN | placeholder;
subsequentSelectSetClause: (EXCEPT | UNION ALL | UNION DISTINCT | INTERSECT | INTERSECT DISTINCT) selectStmtWithParens;
selectSetStmt: selectStmtWithParens (subsequentSelectSetClause)*;
selectStmt:
with=withClause?
SELECT DISTINCT? topClause?
columns=columnExprList
from=fromClause?
arrayJoinClause?
prewhereClause?
where=whereClause?
groupByClause? (WITH (CUBE | ROLLUP))? (WITH TOTALS)?
havingClause?
windowClause?
orderByClause?
limitByClause?
(limitAndOffsetClause | offsetOnlyClause)?
settingsClause?
;
withClause: WITH withExprList;
topClause: TOP DECIMAL_LITERAL (WITH TIES)?;
fromClause: FROM joinExpr;
arrayJoinClause: (LEFT | INNER)? ARRAY JOIN columnExprList;
windowClause: WINDOW identifier AS LPAREN windowExpr RPAREN (COMMA identifier AS LPAREN windowExpr RPAREN)*;
prewhereClause: PREWHERE columnExpr;
whereClause: WHERE columnExpr;
groupByClause: GROUP BY ((CUBE | ROLLUP) LPAREN columnExprList RPAREN | columnExprList);
havingClause: HAVING columnExpr;
orderByClause: ORDER BY orderExprList;
projectionOrderByClause: ORDER BY columnExprList;
limitByClause: LIMIT limitExpr BY columnExprList;
limitAndOffsetClause
: LIMIT columnExpr (COMMA columnExpr)? (WITH TIES)? // compact OFFSET-optional form
| LIMIT columnExpr (WITH TIES)? OFFSET columnExpr // verbose OFFSET-included form with WITH TIES
;
offsetOnlyClause: OFFSET columnExpr;
settingsClause: SETTINGS settingExprList;
joinExpr
: joinExpr joinOp? JOIN joinExpr joinConstraintClause # JoinExprOp
| joinExpr joinOpCross joinExpr # JoinExprCrossOp
| tableExpr FINAL? sampleClause? # JoinExprTable
| LPAREN joinExpr RPAREN # JoinExprParens
;
joinOp
: ((ALL | ANY | ASOF)? INNER | INNER (ALL | ANY | ASOF)? | (ALL | ANY | ASOF)) # JoinOpInner
| ( (SEMI | ALL | ANTI | ANY | ASOF)? (LEFT | RIGHT) OUTER?
| (LEFT | RIGHT) OUTER? (SEMI | ALL | ANTI | ANY | ASOF)?
) # JoinOpLeftRight
| ((ALL | ANY)? FULL OUTER? | FULL OUTER? (ALL | ANY)?) # JoinOpFull
;
joinOpCross
: CROSS JOIN
| COMMA
;
joinConstraintClause
: ON columnExprList
| USING LPAREN columnExprList RPAREN
| USING columnExprList
;
sampleClause: SAMPLE ratioExpr (OFFSET ratioExpr)?;
limitExpr: columnExpr ((COMMA | OFFSET) columnExpr)?;
orderExprList: orderExpr (COMMA orderExpr)*;
orderExpr: columnExpr (ASCENDING | DESCENDING | DESC)? (NULLS (FIRST | LAST))? (COLLATE STRING_LITERAL)?;
ratioExpr: placeholder | numberLiteral (SLASH numberLiteral)?;
settingExprList: settingExpr (COMMA settingExpr)*;
settingExpr: identifier EQ_SINGLE literal;
windowExpr: winPartitionByClause? winOrderByClause? winFrameClause?;
winPartitionByClause: PARTITION BY columnExprList;
winOrderByClause: ORDER BY orderExprList;
winFrameClause: (ROWS | RANGE) winFrameExtend;
winFrameExtend
: winFrameBound # frameStart
| BETWEEN winFrameBound AND winFrameBound # frameBetween
;
winFrameBound: (CURRENT ROW | UNBOUNDED PRECEDING | UNBOUNDED FOLLOWING | numberLiteral PRECEDING | numberLiteral FOLLOWING);
//rangeClause: RANGE LPAREN (MIN identifier MAX identifier | MAX identifier MIN identifier) RPAREN;
// Columns
expr: columnExpr EOF;
columnTypeExpr
: identifier # ColumnTypeExprSimple // UInt64
| identifier LPAREN identifier columnTypeExpr (COMMA identifier columnTypeExpr)* COMMA? RPAREN # ColumnTypeExprNested // Nested
| identifier LPAREN enumValue (COMMA enumValue)* COMMA? RPAREN # ColumnTypeExprEnum // Enum
| identifier LPAREN columnTypeExpr (COMMA columnTypeExpr)* COMMA? RPAREN # ColumnTypeExprComplex // Array, Tuple
| identifier LPAREN columnExprList? RPAREN # ColumnTypeExprParam // FixedString(N)
;
columnExprList: columnExpr (COMMA columnExpr)* COMMA?;
columnExpr
: CASE caseExpr=columnExpr? (WHEN whenExpr=columnExpr THEN thenExpr=columnExpr)+ (ELSE elseExpr=columnExpr)? END # ColumnExprCase
| CAST LPAREN columnExpr AS columnTypeExpr RPAREN # ColumnExprCast
| DATE STRING_LITERAL # ColumnExprDate
// | EXTRACT LPAREN interval FROM columnExpr RPAREN # ColumnExprExtract // Interferes with a function call
| INTERVAL STRING_LITERAL # ColumnExprIntervalString
| INTERVAL columnExpr interval # ColumnExprInterval
| SUBSTRING LPAREN columnExpr FROM columnExpr (FOR columnExpr)? RPAREN # ColumnExprSubstring
| TIMESTAMP STRING_LITERAL # ColumnExprTimestamp
| TRIM LPAREN (BOTH | LEADING | TRAILING) string FROM columnExpr RPAREN # ColumnExprTrim
| identifier (LPAREN columnExprs=columnExprList? RPAREN) (LPAREN DISTINCT? columnArgList=columnExprList? RPAREN)? OVER LPAREN windowExpr RPAREN # ColumnExprWinFunction
| identifier (LPAREN columnExprs=columnExprList? RPAREN) (LPAREN DISTINCT? columnArgList=columnExprList? RPAREN)? OVER identifier # ColumnExprWinFunctionTarget
| identifier (LPAREN columnExprs=columnExprList? RPAREN)? LPAREN DISTINCT? columnArgList=columnExprList? RPAREN # ColumnExprFunction
| columnExpr LPAREN selectSetStmt RPAREN # ColumnExprCallSelect
| columnExpr LPAREN columnExprList? RPAREN # ColumnExprCall
| tSQLxTagElement # ColumnExprTagElement
| templateString # ColumnExprTemplateString
| literal # ColumnExprLiteral
// FIXME(ilezhankin): this part looks very ugly, maybe there is another way to express it
| columnExpr LBRACKET columnExpr RBRACKET # ColumnExprArrayAccess
| columnExpr DOT DECIMAL_LITERAL # ColumnExprTupleAccess
| columnExpr DOT identifier # ColumnExprPropertyAccess
| columnExpr NULL_PROPERTY LBRACKET columnExpr RBRACKET # ColumnExprNullArrayAccess
| columnExpr NULL_PROPERTY DECIMAL_LITERAL # ColumnExprNullTupleAccess
| columnExpr NULL_PROPERTY identifier # ColumnExprNullPropertyAccess
| DASH columnExpr # ColumnExprNegate
| left=columnExpr ( operator=ASTERISK // *
| operator=SLASH // /
| operator=PERCENT // %
) right=columnExpr # ColumnExprPrecedence1
| left=columnExpr ( operator=PLUS // +
| operator=DASH // -
| operator=CONCAT // ||
) right=columnExpr # ColumnExprPrecedence2
| left=columnExpr ( operator=EQ_DOUBLE // =
| operator=EQ_SINGLE // ==
| operator=NOT_EQ // !=
| operator=LT_EQ // <=
| operator=LT // <
| operator=GT_EQ // >=
| operator=GT // >
| operator=NOT? IN COHORT? // in, not in; in cohort; not in cohort
| operator=NOT? (LIKE | ILIKE) // like, not like, ilike, not ilike
| operator=REGEX_SINGLE // ~
| operator=REGEX_DOUBLE // =~
| operator=NOT_REGEX // !~
| operator=IREGEX_SINGLE // ~*
| operator=IREGEX_DOUBLE // =~*
| operator=NOT_IREGEX // !~*
) right=columnExpr # ColumnExprPrecedence3
| columnExpr IS NOT? NULL_SQL # ColumnExprIsNull
| columnExpr NULLISH columnExpr # ColumnExprNullish
| NOT columnExpr # ColumnExprNot
| columnExpr AND columnExpr # ColumnExprAnd
| columnExpr OR columnExpr # ColumnExprOr
// TODO(ilezhankin): `BETWEEN a AND b AND c` is parsed in a wrong way: `BETWEEN (a AND b) AND c`
| columnExpr NOT? BETWEEN columnExpr AND columnExpr # ColumnExprBetween
| <assoc=right> columnExpr QUERY columnExpr COLON columnExpr # ColumnExprTernaryOp
| columnExpr (AS identifier | AS STRING_LITERAL) # ColumnExprAlias
| (tableIdentifier DOT)? ASTERISK # ColumnExprAsterisk // single-column only
| LPAREN selectSetStmt RPAREN # ColumnExprSubquery // single-column only
| LPAREN columnExpr RPAREN # ColumnExprParens // single-column only
| LPAREN columnExprList RPAREN # ColumnExprTuple
| LBRACKET columnExprList? RBRACKET # ColumnExprArray
| LBRACE (kvPairList)? RBRACE # ColumnExprDict
| columnLambdaExpr # ColumnExprLambda
| columnIdentifier # ColumnExprIdentifier
;
columnLambdaExpr:
( LPAREN identifier (COMMA identifier)* COMMA? RPAREN
| identifier (COMMA identifier)* COMMA?
| LPAREN RPAREN
)
ARROW (columnExpr | block)
;
tSQLxChildElement
: tSQLxTagElement
| TSQLX_TEXT_TEXT
| LBRACE columnExpr RBRACE;
tSQLxTagElement
: LT identifier tSQLxTagAttribute* SLASH_GT
| LT identifier tSQLxTagAttribute* GT tSQLxChildElement* LT_SLASH identifier GT
;
tSQLxTagAttribute
: identifier EQ_SINGLE string
| identifier EQ_SINGLE LBRACE columnExpr RBRACE
| identifier
;
withExprList: withExpr (COMMA withExpr)* COMMA?;
withExpr
: identifier AS LPAREN selectSetStmt RPAREN # WithExprSubquery
// NOTE: asterisk and subquery goes before |columnExpr| so that we can mark them as multi-column expressions.
| columnExpr AS identifier # WithExprColumn
;
// This is slightly different in TSQL compared to ClickHouse SQL
// TSQL allows unlimited ("*") nestedIdentifier-s "properties.b.a.a.w.a.s".
// We parse and convert "databaseIdentifier.tableIdentifier.columnIdentifier.nestedIdentifier.*"
// to just one ast.Field(chain=['a','b','columnIdentifier','on','and','on']).
columnIdentifier: placeholder | ((tableIdentifier DOT)? nestedIdentifier);
nestedIdentifier: identifier (DOT identifier)*;
tableExpr
: tableIdentifier # TableExprIdentifier
| tableFunctionExpr # TableExprFunction
| LPAREN selectSetStmt RPAREN # TableExprSubquery
| tableExpr (alias | AS identifier) # TableExprAlias
| tSQLxTagElement # TableExprTag
| placeholder # TableExprPlaceholder
;
tableFunctionExpr: identifier LPAREN tableArgList? RPAREN;
tableIdentifier: (databaseIdentifier DOT)? nestedIdentifier;
tableArgList: columnExpr (COMMA columnExpr)* COMMA?;
// Databases
databaseIdentifier: identifier;
// Basics
floatingLiteral
: FLOATING_LITERAL
| DOT (DECIMAL_LITERAL | OCTAL_LITERAL)
| DECIMAL_LITERAL DOT (DECIMAL_LITERAL | OCTAL_LITERAL)? // can't move this to the lexer or it will break nested tuple access: t.1.2
;
numberLiteral: (PLUS | DASH)? (floatingLiteral | OCTAL_LITERAL | DECIMAL_LITERAL | HEXADECIMAL_LITERAL | INF | NAN_SQL);
literal
: numberLiteral
| STRING_LITERAL
| NULL_SQL
;
interval: SECOND | MINUTE | HOUR | DAY | WEEK | MONTH | QUARTER | YEAR;
keyword
// except NULL_SQL, INF, NAN_SQL
: ALL | AND | ANTI | ANY | ARRAY | AS | ASCENDING | ASOF | BETWEEN | BOTH | BY | CASE
| CAST | COHORT | COLLATE | CROSS | CUBE | CURRENT | DATE | DESC | DESCENDING
| DISTINCT | ELSE | END | EXTRACT | FINAL | FIRST
| FOR | FOLLOWING | FROM | FULL | GROUP | HAVING | ID | IS
| IF | ILIKE | IN | INNER | INTERVAL | JOIN | KEY
| LAST | LEADING | LEFT | LIKE | LIMIT
| NOT | NULLS | OFFSET | ON | OR | ORDER | OUTER | OVER | PARTITION
| PRECEDING | PREWHERE | RANGE | RETURN | RIGHT | ROLLUP | ROW
| ROWS | SAMPLE | SELECT | SEMI | SETTINGS | SUBSTRING
| THEN | TIES | TIMESTAMP | TOTALS | TRAILING | TRIM | TRUNCATE | TO | TOP
| UNBOUNDED | UNION | USING | WHEN | WHERE | WINDOW | WITH
;
keywordForAlias
: DATE | FIRST | ID | KEY
;
alias: IDENTIFIER | keywordForAlias; // |interval| can't be an alias, otherwise 'INTERVAL 1 SOMETHING' becomes ambiguous.
identifier: IDENTIFIER | interval | keyword;
enumValue: string EQ_SINGLE numberLiteral;
placeholder: LBRACE columnExpr RBRACE;
string: STRING_LITERAL | templateString;
templateString : QUOTE_SINGLE_TEMPLATE stringContents* QUOTE_SINGLE ;
stringContents : STRING_ESCAPE_TRIGGER columnExpr RBRACE | STRING_TEXT;
// These are magic "full template strings", which are used to parse "full text field" templates without the surrounding SQL.
// We will need to add F' to the start of the string to change the lexer's mode.
fullTemplateString: QUOTE_SINGLE_TEMPLATE_FULL stringContentsFull* EOF ;
stringContentsFull : FULL_STRING_ESCAPE_TRIGGER columnExpr RBRACE | FULL_STRING_TEXT;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,208 @@
ALL=1
AND=2
ANTI=3
ANY=4
ARRAY=5
AS=6
ASCENDING=7
ASOF=8
BETWEEN=9
BOTH=10
BY=11
CASE=12
CAST=13
CATCH=14
COHORT=15
COLLATE=16
CROSS=17
CUBE=18
CURRENT=19
DATE=20
DAY=21
DESC=22
DESCENDING=23
DISTINCT=24
ELSE=25
END=26
EXCEPT=27
EXTRACT=28
FINAL=29
FINALLY=30
FIRST=31
FN=32
FOLLOWING=33
FOR=34
FROM=35
FULL=36
FUN=37
GROUP=38
HAVING=39
HOUR=40
ID=41
IF=42
ILIKE=43
IN=44
INF=45
INNER=46
INTERSECT=47
INTERVAL=48
IS=49
JOIN=50
KEY=51
LAST=52
LEADING=53
LEFT=54
LET=55
LIKE=56
LIMIT=57
MINUTE=58
MONTH=59
NAN_SQL=60
NOT=61
NULL_SQL=62
NULLS=63
OFFSET=64
ON=65
OR=66
ORDER=67
OUTER=68
OVER=69
PARTITION=70
PRECEDING=71
PREWHERE=72
QUARTER=73
RANGE=74
RETURN=75
RIGHT=76
ROLLUP=77
ROW=78
ROWS=79
SAMPLE=80
SECOND=81
SELECT=82
SEMI=83
SETTINGS=84
SUBSTRING=85
THEN=86
THROW=87
TIES=88
TIMESTAMP=89
TO=90
TOP=91
TOTALS=92
TRAILING=93
TRIM=94
TRUNCATE=95
TRY=96
UNBOUNDED=97
UNION=98
USING=99
WEEK=100
WHEN=101
WHERE=102
WHILE=103
WINDOW=104
WITH=105
YEAR=106
ESCAPE_CHAR_COMMON=107
IDENTIFIER=108
FLOATING_LITERAL=109
OCTAL_LITERAL=110
DECIMAL_LITERAL=111
HEXADECIMAL_LITERAL=112
STRING_LITERAL=113
ARROW=114
ASTERISK=115
BACKQUOTE=116
BACKSLASH=117
COLON=118
COMMA=119
CONCAT=120
DASH=121
DOLLAR=122
DOT=123
EQ_DOUBLE=124
EQ_SINGLE=125
GT_EQ=126
GT=127
HASH=128
IREGEX_SINGLE=129
IREGEX_DOUBLE=130
LBRACE=131
LBRACKET=132
LPAREN=133
LT_EQ=134
LT=135
LT_SLASH=136
NOT_EQ=137
NOT_IREGEX=138
NOT_REGEX=139
NULL_PROPERTY=140
NULLISH=141
PERCENT=142
PLUS=143
QUERY=144
QUOTE_DOUBLE=145
QUOTE_SINGLE_TEMPLATE=146
QUOTE_SINGLE_TEMPLATE_FULL=147
QUOTE_SINGLE=148
REGEX_SINGLE=149
REGEX_DOUBLE=150
RBRACE=151
RBRACKET=152
RPAREN=153
SEMICOLON=154
SLASH=155
SLASH_GT=156
UNDERSCORE=157
MULTI_LINE_COMMENT=158
SINGLE_LINE_COMMENT=159
WHITESPACE=160
STRING_TEXT=161
STRING_ESCAPE_TRIGGER=162
FULL_STRING_TEXT=163
FULL_STRING_ESCAPE_TRIGGER=164
TAG_WS=165
TAGC_WS=166
TSQLX_TEXT_TEXT=167
TSQLX_TEXT_WS=168
'->'=114
'*'=115
'`'=116
'\\'=117
':'=118
','=119
'||'=120
'-'=121
'$'=122
'.'=123
'=='=124
'>='=126
'#'=128
'~*'=129
'=~*'=130
'{'=131
'['=132
'('=133
'<='=134
'<'=135
'</'=136
'!~*'=138
'!~'=139
'?.'=140
'??'=141
'%'=142
'+'=143
'?'=144
'"'=145
'f\''=146
'F\''=147
'\''=148
'~'=149
'=~'=150
'}'=151
']'=152
')'=153
';'=154
'/'=155
'_'=157
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,166 @@
import { describe, it, expect } from "vitest";
import { CharStreams, CommonTokenStream } from "antlr4ts";
import { TSQLLexer } from "./TSQLLexer.js";
import { TSQLParser } from "./TSQLParser.js";
describe("TSQLParser", () => {
function parse(input: string) {
const inputStream = CharStreams.fromString(input);
const lexer = new TSQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
const parser = new TSQLParser(tokenStream);
return parser;
}
describe("select statements", () => {
it("should parse a simple SELECT statement", () => {
const parser = parse("SELECT * FROM users;");
const tree = parser.select();
expect(tree).toBeDefined();
// The select rule can return selectStmt, selectSetStmt, or tSQLxTagElement
// Most SELECT statements are wrapped in selectSetStmt
const selectSetStmt = tree.selectSetStmt();
expect(selectSetStmt).toBeDefined();
// Get the underlying selectStmt from selectSetStmt -> selectStmtWithParens
const selectStmtWithParens = selectSetStmt!.selectStmtWithParens();
const selectStmt = selectStmtWithParens.selectStmt();
expect(selectStmt).toBeDefined();
expect(selectStmt!.SELECT()).toBeDefined();
expect(selectStmt!.columnExprList()).toBeDefined();
expect(selectStmt!.fromClause()).toBeDefined();
});
it("should parse SELECT with WHERE clause", () => {
const parser = parse("SELECT id, name FROM users WHERE id = 1;");
const tree = parser.select();
expect(tree).toBeDefined();
const selectSetStmt = tree.selectSetStmt();
const selectStmt = selectSetStmt!.selectStmtWithParens().selectStmt()!;
expect(selectStmt.whereClause()).toBeDefined();
expect(selectStmt.whereClause()!.WHERE()).toBeDefined();
expect(selectStmt.whereClause()!.columnExpr()).toBeDefined();
});
it("should parse SELECT with multiple columns", () => {
const parser = parse("SELECT id, name, email FROM users;");
const tree = parser.select();
expect(tree).toBeDefined();
const selectSetStmt = tree.selectSetStmt();
const selectStmt = selectSetStmt!.selectStmtWithParens().selectStmt()!;
const columnList = selectStmt.columnExprList();
expect(columnList).toBeDefined();
// columnExprList contains comma-separated expressions
expect(columnList.columnExpr().length).toBeGreaterThanOrEqual(1);
});
it("should parse SELECT with DISTINCT", () => {
const parser = parse("SELECT DISTINCT id FROM users;");
const tree = parser.select();
expect(tree).toBeDefined();
const selectSetStmt = tree.selectSetStmt();
const selectStmt = selectSetStmt!.selectStmtWithParens().selectStmt()!;
expect(selectStmt.DISTINCT()).toBeDefined();
});
});
describe("expressions", () => {
it("should parse a simple addition expression", () => {
const parser = parse("1 + 2");
const tree = parser.expr();
expect(tree).toBeDefined();
const columnExpr = tree.columnExpr();
expect(columnExpr).toBeDefined();
// Check that the expression has children (the operands and operator)
expect(columnExpr.text).toBeDefined();
const text = columnExpr.text;
expect(text).toContain("1");
expect(text).toContain("2");
expect(text).toContain("+");
});
it("should parse arithmetic expressions with parentheses", () => {
const parser = parse("(1 + 2) * 3");
const tree = parser.expr();
expect(tree).toBeDefined();
const columnExpr = tree.columnExpr();
expect(columnExpr).toBeDefined();
// The expression should contain the operators
const text = columnExpr.text;
expect(text).toContain("+");
expect(text).toContain("*");
expect(text).toContain("1");
expect(text).toContain("2");
expect(text).toContain("3");
});
it("should parse string literals", () => {
const parser = parse("'hello world'");
const tree = parser.expr();
expect(tree).toBeDefined();
const columnExpr = tree.columnExpr();
expect(columnExpr).toBeDefined();
const text = columnExpr.text;
expect(text).toContain("hello");
expect(text).toContain("world");
});
it("should parse numeric literals", () => {
const parser = parse("42");
const tree = parser.expr();
expect(tree).toBeDefined();
const columnExpr = tree.columnExpr();
expect(columnExpr).toBeDefined();
expect(columnExpr.text).toContain("42");
});
});
describe("program", () => {
it("should parse an empty program", () => {
const parser = parse("");
const tree = parser.program();
expect(tree).toBeDefined();
expect(tree.EOF()).toBeDefined();
expect(tree.declaration().length).toBe(0);
});
it("should parse variable declarations", () => {
const parser = parse("let x := 1");
const tree = parser.program();
expect(tree).toBeDefined();
expect(tree.declaration().length).toBe(1);
const declaration = tree.declaration(0);
// Check if it's a varDecl
const varDecl = declaration.varDecl();
expect(varDecl).toBeDefined();
expect(varDecl!.LET()).toBeDefined();
expect(varDecl!.identifier()).toBeDefined();
expect(varDecl!.expression()).toBeDefined();
});
it("should parse multiple declarations", () => {
const parser = parse("let x := 1; let y := 2");
const tree = parser.program();
expect(tree).toBeDefined();
// Count only non-empty declarations (varDecl or non-empty statements)
const varDecls = tree.declaration().filter((d) => d.varDecl() !== undefined);
expect(varDecls.length).toBe(2);
});
});
});