Skip to content

Commit cf67898

Browse files
committed
perf: build IN-list AST children in linear time
The IN-list parser previously called AstNode::add_child for every value. Each append walked all existing siblings, making construction quadratic in the number of list elements even though tokenization itself is linear. Keep a tail pointer local to parse_in and attach each parsed expression in constant time. Preserve the left-hand expression, value order, nested expression roots and the separate subquery path. Both PostgreSQL and MySQL benefit without adding allocations or changing the 48-byte AST ABI. Add regression tests inspecting every value in 3,000-element lists across arena blocks, together with emission checks for NOT IN, NULL, functions, arithmetic, tuples, nested lists and subqueries in both dialects. These correctness tests passed in the full suite; timing thresholds are not part of the tests. Benchmark code and reports are intentionally excluded from this branch's commits.
1 parent eeee5ef commit cf67898

2 files changed

Lines changed: 83 additions & 1 deletion

File tree

‎include/sql_parser/expression_parser.h‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,9 +542,15 @@ class ExpressionParser {
542542
AstNode* sq = parse_subquery_inner();
543543
node->add_child(sq);
544544
} else {
545+
// Expression roots are single, unattached nodes. Keep the tail
546+
// locally so appending N IN-list values takes O(N), not O(N^2).
547+
AstNode* tail = left;
545548
while (true) {
546549
AstNode* val = parse();
547-
if (val) node->add_child(val);
550+
if (val) {
551+
tail->next_sibling = val;
552+
tail = val;
553+
}
548554
if (tok_.peek().type == TokenType::TK_COMMA) {
549555
tok_.skip();
550556
} else {

‎tests/test_expression.cpp‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <gtest/gtest.h>
22
#include "sql_parser/parser.h"
33
#include "sql_parser/expression_parser.h"
4+
#include "sql_parser/emitter.h"
45

56
using namespace sql_parser;
67

@@ -292,6 +293,81 @@ TEST_F(ExpressionTest, InList) {
292293
EXPECT_EQ(count, 4);
293294
}
294295

296+
template <Dialect D>
297+
static void check_large_in_list() {
298+
// Cross arena blocks and inspect every value: appending must neither drop
299+
// elements nor reorder/overwrite the left-hand expression or list tail.
300+
constexpr int count = 3000;
301+
std::string sql = "key_col IN (";
302+
for (int i = 0; i < count; ++i) {
303+
if (i) sql += ", ";
304+
sql += std::to_string(i);
305+
}
306+
sql += ")";
307+
Arena arena;
308+
Tokenizer<D> tok;
309+
tok.reset(sql.data(), sql.size());
310+
ExpressionParser<D> parser(tok, arena);
311+
AstNode* node = parser.parse();
312+
ASSERT_NE(node, nullptr);
313+
ASSERT_EQ(node->type, NodeType::NODE_IN_LIST);
314+
ASSERT_NE(node->first_child, nullptr);
315+
EXPECT_EQ(std::string(node->first_child->value_ptr, node->first_child->value_len), "key_col");
316+
const AstNode* value = node->first_child->next_sibling;
317+
for (int i = 0; i < count; ++i) {
318+
ASSERT_NE(value, nullptr) << "Missing item " << i;
319+
EXPECT_EQ(value->type, NodeType::NODE_LITERAL_INT);
320+
EXPECT_EQ(std::string(value->value_ptr, value->value_len), std::to_string(i));
321+
value = value->next_sibling;
322+
}
323+
EXPECT_EQ(value, nullptr);
324+
EXPECT_EQ(tok.peek().type, TokenType::TK_EOF);
325+
Emitter<D> emitter(arena);
326+
emitter.emit(node);
327+
StringRef result = emitter.result();
328+
EXPECT_EQ(std::string(result.ptr, result.len), sql);
329+
}
330+
331+
TEST(InListAppend, LargeMySQLListRetainsEveryValue) {
332+
check_large_in_list<Dialect::MySQL>();
333+
}
334+
335+
TEST(InListAppend, LargePostgreSQLListRetainsEveryValue) {
336+
check_large_in_list<Dialect::PostgreSQL>();
337+
}
338+
339+
template <Dialect D>
340+
static void check_nested_in_lists() {
341+
const char* cases[] = {
342+
"x IN (1)",
343+
"x NOT IN (1, NULL, f(2, 3), 4 + 5)",
344+
"(x, y) IN ((1, 2), (3, 4))",
345+
"x IN (1, (y IN (2, 3)), 4)",
346+
"x IN (SELECT y FROM t WHERE z IN (2, 3))",
347+
};
348+
Parser<D> parser;
349+
for (const char* expression : cases) {
350+
const std::string sql = std::string("SELECT * FROM t WHERE ") + expression;
351+
SCOPED_TRACE(sql);
352+
auto result = parser.parse(sql.data(), sql.size());
353+
ASSERT_EQ(result.status, ParseResult::OK);
354+
ASSERT_TRUE(result.full_input);
355+
ASSERT_NE(result.ast, nullptr);
356+
Emitter<D> emitter(parser.arena());
357+
emitter.emit(result.ast);
358+
StringRef emitted = emitter.result();
359+
EXPECT_EQ(std::string(emitted.ptr, emitted.len), sql);
360+
}
361+
}
362+
363+
TEST(InListAppend, NestedMySQLListsAndSubqueryRoundTrip) {
364+
check_nested_in_lists<Dialect::MySQL>();
365+
}
366+
367+
TEST(InListAppend, NestedPostgreSQLListsAndSubqueryRoundTrip) {
368+
check_nested_in_lists<Dialect::PostgreSQL>();
369+
}
370+
295371
TEST_F(ExpressionTest, FunctionCall) {
296372
AstNode* node = parse_expr("COUNT(*)");
297373
ASSERT_NE(node, nullptr);

0 commit comments

Comments
 (0)