@@ -41,6 +41,24 @@ using SubqueryParseCallback = AstNode*(*)(Tokenizer<D>&, Arena&);
4141template <Dialect D>
4242class ExpressionParser {
4343public:
44+ // Keyword operators are stored under their canonical spelling.
45+ static StringRef canonical_op (const Token& op) {
46+ switch (op.type ) {
47+ case TokenType::TK_AND : return StringRef{" AND" , 3 };
48+ case TokenType::TK_OR : return StringRef{" OR" , 2 };
49+ case TokenType::TK_XOR : return StringRef{" XOR" , 3 };
50+ case TokenType::TK_NOT : return StringRef{" NOT" , 3 };
51+ case TokenType::TK_IS : return StringRef{" IS" , 2 };
52+ case TokenType::TK_IN : return StringRef{" IN" , 2 };
53+ case TokenType::TK_LIKE : return StringRef{" LIKE" , 4 };
54+ case TokenType::TK_REGEXP : return StringRef{" REGEXP" , 6 };
55+ case TokenType::TK_DIV : return StringRef{" DIV" , 3 };
56+ case TokenType::TK_MOD : return StringRef{" MOD" , 3 };
57+ case TokenType::TK_BETWEEN : return StringRef{" BETWEEN" , 7 };
58+ default : return op.text ;
59+ }
60+ }
61+
4462 ExpressionParser (Tokenizer<D>& tokenizer, Arena& arena)
4563 : tok_(tokenizer), arena_(arena) {}
4664
@@ -120,7 +138,14 @@ class ExpressionParser {
120138 }
121139 case TokenType::TK_NULL : {
122140 tok_.skip ();
123- return make_node_from_token (arena_, NodeType::NODE_LITERAL_NULL , t);
141+ {
142+ // Keep the source span lossless, but store the keyword under
143+ // its canonical spelling.
144+ AstNode* null_node =
145+ make_node_from_token (arena_, NodeType::NODE_LITERAL_NULL , t);
146+ if (null_node) null_node->set_value (StringRef{" NULL" , 4 });
147+ return null_node;
148+ }
124149 }
125150 case TokenType::TK_TRUE :
126151 case TokenType::TK_FALSE : {
@@ -186,7 +211,7 @@ class ExpressionParser {
186211 tok_.skip ();
187212 AstNode* operand = parse (Precedence::UNARY );
188213 if (!operand) return nullptr ;
189- AstNode* node = make_node (arena_, NodeType::NODE_UNARY_OP , t. text );
214+ AstNode* node = make_node (arena_, NodeType::NODE_UNARY_OP , canonical_op (t) );
190215 set_span_through_node_ (node, t.source , operand);
191216 node->add_child (operand);
192217 return node;
@@ -196,7 +221,7 @@ class ExpressionParser {
196221 tok_.skip ();
197222 AstNode* operand = parse (Precedence::UNARY );
198223 if (!operand) return nullptr ;
199- AstNode* node = make_node (arena_, NodeType::NODE_UNARY_OP , t. text );
224+ AstNode* node = make_node (arena_, NodeType::NODE_UNARY_OP , canonical_op (t) );
200225 set_span_through_node_ (node, t.source , operand);
201226 node->add_child (operand);
202227 return node;
@@ -205,7 +230,7 @@ class ExpressionParser {
205230 tok_.skip ();
206231 AstNode* operand = parse (Precedence::NOT );
207232 if (!operand) return nullptr ;
208- AstNode* node = make_node (arena_, NodeType::NODE_UNARY_OP , t. text );
233+ AstNode* node = make_node (arena_, NodeType::NODE_UNARY_OP , canonical_op (t) );
209234 set_span_through_node_ (node, t.source , operand);
210235 node->add_child (operand);
211236 return node;
@@ -326,7 +351,16 @@ class ExpressionParser {
326351 // Check for function call: name(
327352 if (tok_.peek ().type == TokenType::TK_LPAREN ) {
328353 tok_.skip (); // consume (
329- AstNode* func = make_node (arena_, NodeType::NODE_FUNCTION_CALL , name_token.text );
354+ // Function names are case-insensitive, so store them under a canonical spelling.
355+ StringRef func_name = name_token.text ;
356+ if constexpr (D == Dialect::MySQL) {
357+ // MySQL folds all function names up:
358+ func_name = arena_.allocate_upper (func_name);
359+ } else if (!token_was_delimited_ (name_token)) {
360+ // PostgreSQL folds undelimited function names down:
361+ func_name = arena_.allocate_lower (func_name);
362+ }
363+ AstNode* func = make_node (arena_, NodeType::NODE_FUNCTION_CALL , func_name);
330364 // CAST uses `CAST(expr AS type)` rather than a comma-separated
331365 // argument list. Model it as a function call so consumers can
332366 // reject or handle the expression without leaving valid input
@@ -449,25 +483,25 @@ class ExpressionParser {
449483 tok_.skip ();
450484 AstNode* in_node = parse_in (left);
451485 // Wrap in NOT
452- AstNode* not_node = make_node (arena_, NodeType::NODE_UNARY_OP , op. text );
486+ AstNode* not_node = make_node (arena_, NodeType::NODE_UNARY_OP , canonical_op (op) );
453487 not_node->add_child (in_node);
454488 return not_node;
455489 }
456490 if (actual_op.type == TokenType::TK_BETWEEN ) {
457491 tok_.skip ();
458492 AstNode* between_node = parse_between (left);
459- AstNode* not_node = make_node (arena_, NodeType::NODE_UNARY_OP , op. text );
493+ AstNode* not_node = make_node (arena_, NodeType::NODE_UNARY_OP , canonical_op (op) );
460494 not_node->add_child (between_node);
461495 return not_node;
462496 }
463497 if (actual_op.type == TokenType::TK_LIKE ||
464498 actual_op.type == TokenType::TK_REGEXP ) {
465499 tok_.skip ();
466500 AstNode* right = parse (prec);
467- AstNode* like_node = make_node (arena_, NodeType::NODE_BINARY_OP , actual_op. text );
501+ AstNode* like_node = make_node (arena_, NodeType::NODE_BINARY_OP , canonical_op ( actual_op) );
468502 like_node->add_child (left);
469503 if (right) like_node->add_child (right);
470- AstNode* not_node = make_node (arena_, NodeType::NODE_UNARY_OP , op. text );
504+ AstNode* not_node = make_node (arena_, NodeType::NODE_UNARY_OP , canonical_op (op) );
471505 not_node->add_child (like_node);
472506 return not_node;
473507 }
@@ -511,7 +545,7 @@ class ExpressionParser {
511545 // Standard binary operator
512546 AstNode* right = parse (prec);
513547 if (!right) return left;
514- AstNode* node = make_node (arena_, NodeType::NODE_BINARY_OP , op. text );
548+ AstNode* node = make_node (arena_, NodeType::NODE_BINARY_OP , canonical_op (op) );
515549 node->add_child (left);
516550 node->add_child (right);
517551 return node;
@@ -758,7 +792,9 @@ class ExpressionParser {
758792 Token dir = tok_.peek ();
759793 if (dir.type == TokenType::TK_ASC || dir.type == TokenType::TK_DESC ) {
760794 tok_.skip ();
761- item->add_child (make_node (arena_, NodeType::NODE_IDENTIFIER , dir.text ));
795+ item->add_child (make_node (arena_, NodeType::NODE_IDENTIFIER ,
796+ dir.type == TokenType::TK_ASC ? StringRef{" ASC" , 3 }
797+ : StringRef{" DESC" , 4 }));
762798 }
763799 ord->add_child (item);
764800 if (tok_.peek ().type == TokenType::TK_COMMA ) tok_.skip ();
0 commit comments