Skip to content

Commit e3f494d

Browse files
committed
Merge branch 'main' into feat/postgresql-compat-harness
2 parents 901f666 + af1e5b0 commit e3f494d

21 files changed

Lines changed: 1404 additions & 89 deletions

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ TEST_SRCS = $(TEST_DIR)/test_main.cpp \
4343
$(TEST_DIR)/test_classifier.cpp \
4444
$(TEST_DIR)/test_expression.cpp \
4545
$(TEST_DIR)/test_set.cpp \
46+
$(TEST_DIR)/test_user_variable.cpp \
4647
$(TEST_DIR)/test_select.cpp \
4748
$(TEST_DIR)/test_emitter.cpp \
4849
$(TEST_DIR)/test_stmt_cache.cpp \

include/sql_engine/local_txn.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <unordered_map>
1414
#include <cstring>
1515
#include <memory>
16+
#include <algorithm>
1617

1718
namespace sql_engine {
1819

include/sql_parser/ast.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "sql_parser/common.h"
55
#include "sql_parser/arena.h"
6+
#include "sql_parser/token.h"
67
#include <cstdint>
78
#include <type_traits>
89

@@ -12,17 +13,25 @@ struct AstNode {
1213
AstNode* first_child;
1314
AstNode* next_sibling;
1415
const char* value_ptr;
16+
const char* source_ptr;
1517
uint32_t value_len;
18+
uint32_t source_len;
1619
NodeType type;
1720
uint16_t flags;
1821

1922
StringRef value() const { return StringRef{value_ptr, value_len}; }
23+
StringRef source() const { return StringRef{source_ptr, source_len}; }
2024

2125
void set_value(StringRef ref) {
2226
value_ptr = ref.ptr;
2327
value_len = ref.len;
2428
}
2529

30+
void set_source(StringRef ref) {
31+
source_ptr = ref.ptr;
32+
source_len = ref.len;
33+
}
34+
2635
void add_child(AstNode* child) {
2736
if (!child) return;
2837
if (!first_child) {
@@ -34,7 +43,7 @@ struct AstNode {
3443
last->next_sibling = child;
3544
}
3645
};
37-
static_assert(sizeof(AstNode) == 32, "AstNode must be 32 bytes");
46+
static_assert(sizeof(AstNode) == 48, "AstNode layout changed unexpectedly");
3847
static_assert(std::is_trivially_copyable_v<AstNode>);
3948

4049
inline AstNode* make_node(Arena& arena, NodeType type, StringRef value = {},
@@ -48,6 +57,13 @@ inline AstNode* make_node(Arena& arena, NodeType type, StringRef value = {},
4857
return node;
4958
}
5059

60+
inline AstNode* make_node_from_token(Arena& arena, NodeType type,
61+
const Token& token, uint16_t flags = 0) {
62+
AstNode* node = make_node(arena, type, token.text, flags);
63+
if (node) node->set_source(token.source);
64+
return node;
65+
}
66+
5167
} // namespace sql_parser
5268

5369
#endif // SQL_PARSER_AST_H

include/sql_parser/common.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,12 @@ enum class NodeType : uint16_t {
231231
NODE_SET_ROLE, // SET [LOCAL] ROLE <name>|NONE|DEFAULT
232232
NODE_SET_SESSION_AUTHORIZATION, // SET SESSION AUTHORIZATION <name>|DEFAULT
233233
NODE_SET_CONSTRAINTS, // SET CONSTRAINTS {ALL|<name>[,...]} {DEFERRED|IMMEDIATE}
234+
235+
// MySQL lossless user-variable/literal nodes. Keep appended so existing
236+
// enum values remain stable for consumers that index by NodeType.
237+
NODE_USER_VARIABLE,
238+
NODE_LITERAL_HEX,
239+
NODE_LITERAL_BIT,
234240
};
235241

236242
} // namespace sql_parser

include/sql_parser/digest.h

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,17 @@ class Digest {
6363

6464
// Helper: check if a token type is a keyword (not an identifier, literal, or operator)
6565
static bool is_keyword_token(TokenType type) {
66-
// Keywords start at TK_SELECT and go through TK_EXCEPT
67-
return static_cast<uint16_t>(type) >= static_cast<uint16_t>(TokenType::TK_SELECT);
66+
return static_cast<uint16_t>(type) >= static_cast<uint16_t>(TokenType::TK_SELECT) &&
67+
static_cast<uint16_t>(type) <= static_cast<uint16_t>(TokenType::TK_RECURSIVE);
6868
}
6969

7070
// Helper: check if a token type is a literal value that should become ?
7171
static bool is_literal_token(TokenType type) {
7272
return type == TokenType::TK_INTEGER ||
7373
type == TokenType::TK_FLOAT ||
74-
type == TokenType::TK_STRING;
74+
type == TokenType::TK_STRING ||
75+
type == TokenType::TK_HEX_LITERAL ||
76+
type == TokenType::TK_BIT_LITERAL;
7577
}
7678

7779
// Helper: uppercase a character
@@ -104,7 +106,12 @@ class Digest {
104106

105107
// Emit a single token to the string builder, uppercasing keywords, replacing literals with ?
106108
void emit_token(StringBuilder& sb, const Token& t, TokenType prev) {
107-
bool space = (prev != TokenType::TK_EOF) && needs_space_before(prev, t.type);
109+
bool quoted_user_after_account = t.type == TokenType::TK_USER_VARIABLE &&
110+
t.source.len >= 2 &&
111+
(t.source.ptr[1] == '\'' || t.source.ptr[1] == '"' || t.source.ptr[1] == '`') &&
112+
(prev == TokenType::TK_STRING || prev == TokenType::TK_QUESTION);
113+
bool space = (prev != TokenType::TK_EOF) &&
114+
!quoted_user_after_account && needs_space_before(prev, t.type);
108115
if (space) sb.append_char(' ');
109116

110117
if (is_literal_token(t.type)) {
@@ -115,6 +122,13 @@ class Digest {
115122
sb.append(t.text.ptr, t.text.len);
116123
} else if (t.type == TokenType::TK_QUESTION) {
117124
sb.append_char('?');
125+
} else if (t.type == TokenType::TK_USER_VARIABLE) {
126+
if (t.source.len >= 2 &&
127+
(t.source.ptr[1] == '\'' || t.source.ptr[1] == '"' || t.source.ptr[1] == '`')) {
128+
sb.append("@?", 2);
129+
} else {
130+
sb.append(t.source);
131+
}
118132
} else if (t.type == TokenType::TK_COMMA) {
119133
sb.append(",", 1);
120134
} else {

include/sql_parser/emitter.h

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ class Emitter {
122122
case NodeType::NODE_ARRAY_CONSTRUCTOR: emit_array_constructor(node); break;
123123
case NodeType::NODE_ARRAY_SUBSCRIPT: emit_array_subscript(node); break;
124124
case NodeType::NODE_FIELD_ACCESS: emit_field_access(node); break;
125-
case NodeType::NODE_SUBQUERY: emit_value(node); break;
125+
case NodeType::NODE_SUBQUERY: emit_subquery(node); break;
126+
case NodeType::NODE_EXPRESSION: emit_parenthesized_expression(node); break;
127+
case NodeType::NODE_USER_VARIABLE: emit_user_variable(node); break;
126128

127129
// ---- Leaf nodes (emit value directly) ----
128130
case NodeType::NODE_PLACEHOLDER:
@@ -131,6 +133,8 @@ class Emitter {
131133
// ---- Leaf nodes (emit value directly) ----
132134
case NodeType::NODE_LITERAL_INT:
133135
case NodeType::NODE_LITERAL_FLOAT:
136+
case NodeType::NODE_LITERAL_HEX:
137+
case NodeType::NODE_LITERAL_BIT:
134138
if (mode_ == EmitMode::DIGEST) { sb_.append_char('?'); break; }
135139
emit_value(node); break;
136140
case NodeType::NODE_LITERAL_NULL:
@@ -152,6 +156,30 @@ class Emitter {
152156
sb_.append(node->value_ptr, node->value_len);
153157
}
154158

159+
void emit_user_variable(const AstNode* node) {
160+
if (mode_ == EmitMode::DIGEST) {
161+
StringRef source = node->source();
162+
if (source.len >= 2 &&
163+
(source.ptr[1] == '\'' || source.ptr[1] == '"' || source.ptr[1] == '`')) {
164+
sb_.append("@?", 2);
165+
return;
166+
}
167+
}
168+
StringRef source = node->source();
169+
if (!source.empty()) {
170+
sb_.append(source.ptr, source.len);
171+
return;
172+
}
173+
sb_.append_char('@');
174+
emit_value(node);
175+
}
176+
177+
void emit_parenthesized_expression(const AstNode* node) {
178+
sb_.append_char('(');
179+
if (node->first_child) emit_node(node->first_child);
180+
sb_.append_char(')');
181+
}
182+
155183
void emit_string_literal(const AstNode* node) {
156184
sb_.append_char('\'');
157185
sb_.append(node->value_ptr, node->value_len);
@@ -1061,10 +1089,28 @@ class Emitter {
10611089
}
10621090

10631091
void emit_unary_op(const AstNode* node) {
1092+
const AstNode* child = node->first_child;
1093+
if (is_not_operator(node) && child) {
1094+
if (child->type == NodeType::NODE_IN_LIST) {
1095+
emit_not_in_list(child);
1096+
return;
1097+
}
1098+
if (child->type == NodeType::NODE_BETWEEN) {
1099+
emit_not_between(child);
1100+
return;
1101+
}
1102+
if (child->type == NodeType::NODE_BINARY_OP &&
1103+
(child->value().equals_ci("LIKE", 4) ||
1104+
child->value().equals_ci("REGEXP", 6))) {
1105+
emit_not_binary_op(child);
1106+
return;
1107+
}
1108+
}
1109+
10641110
emit_value(node);
10651111
// Add space for keyword operators like NOT, no space for - or +
10661112
if (node->value_len > 1) sb_.append_char(' ');
1067-
if (node->first_child) emit_node(node->first_child);
1113+
if (child) emit_node(child);
10681114
}
10691115

10701116
void emit_function_call(const AstNode* node) {
@@ -1102,13 +1148,20 @@ class Emitter {
11021148

11031149
void emit_in_list(const AstNode* node) {
11041150
const AstNode* expr = node->first_child;
1151+
const AstNode* first_val = expr ? expr->next_sibling : nullptr;
11051152
if (expr) emit_node(expr);
1153+
if (first_val && first_val->type == NodeType::NODE_SUBQUERY &&
1154+
!first_val->next_sibling) {
1155+
sb_.append(" IN ");
1156+
emit_node(first_val);
1157+
return;
1158+
}
11061159
sb_.append(" IN (");
11071160
if (mode_ == EmitMode::DIGEST) {
11081161
sb_.append_char('?');
11091162
} else {
11101163
bool first = true;
1111-
for (const AstNode* val = expr ? expr->next_sibling : nullptr; val; val = val->next_sibling) {
1164+
for (const AstNode* val = first_val; val; val = val->next_sibling) {
11121165
if (!first) sb_.append(", ");
11131166
first = false;
11141167
emit_node(val);
@@ -1117,6 +1170,68 @@ class Emitter {
11171170
sb_.append_char(')');
11181171
}
11191172

1173+
void emit_subquery(const AstNode* node) {
1174+
if (node->flags == 1) {
1175+
sb_.append("EXISTS ");
1176+
}
1177+
sb_.append_char('(');
1178+
if (node->first_child) {
1179+
emit_node(node->first_child);
1180+
} else {
1181+
emit_value(node);
1182+
}
1183+
sb_.append_char(')');
1184+
}
1185+
1186+
bool is_not_operator(const AstNode* node) const {
1187+
return node && node->value().equals_ci("NOT", 3);
1188+
}
1189+
1190+
void emit_not_in_list(const AstNode* node) {
1191+
const AstNode* expr = node->first_child;
1192+
const AstNode* first_val = expr ? expr->next_sibling : nullptr;
1193+
if (expr) emit_node(expr);
1194+
if (first_val && first_val->type == NodeType::NODE_SUBQUERY &&
1195+
!first_val->next_sibling) {
1196+
sb_.append(" NOT IN ");
1197+
emit_node(first_val);
1198+
return;
1199+
}
1200+
sb_.append(" NOT IN (");
1201+
if (mode_ == EmitMode::DIGEST) {
1202+
sb_.append_char('?');
1203+
} else {
1204+
bool first = true;
1205+
for (const AstNode* val = first_val; val; val = val->next_sibling) {
1206+
if (!first) sb_.append(", ");
1207+
first = false;
1208+
emit_node(val);
1209+
}
1210+
}
1211+
sb_.append_char(')');
1212+
}
1213+
1214+
void emit_not_between(const AstNode* node) {
1215+
const AstNode* expr = node->first_child;
1216+
const AstNode* low = expr ? expr->next_sibling : nullptr;
1217+
const AstNode* high = low ? low->next_sibling : nullptr;
1218+
if (expr) emit_node(expr);
1219+
sb_.append(" NOT BETWEEN ");
1220+
if (low) emit_node(low);
1221+
sb_.append(" AND ");
1222+
if (high) emit_node(high);
1223+
}
1224+
1225+
void emit_not_binary_op(const AstNode* node) {
1226+
const AstNode* left = node->first_child;
1227+
const AstNode* right = left ? left->next_sibling : nullptr;
1228+
if (left) emit_node(left);
1229+
sb_.append(" NOT ");
1230+
emit_value(node);
1231+
sb_.append_char(' ');
1232+
if (right) emit_node(right);
1233+
}
1234+
11201235
void emit_case_when(const AstNode* node) {
11211236
sb_.append("CASE ");
11221237
for (const AstNode* child = node->first_child; child; child = child->next_sibling) {

0 commit comments

Comments
 (0)