Skip to content

Commit bdf10fc

Browse files
committed
feat: implement ARRAY subscript evaluation in expression evaluator
Replace the NODE_ARRAY_SUBSCRIPT stub with real evaluation logic that walks the ARRAY constructor's child nodes to retrieve the element at the given index. PostgreSQL uses 1-based indexing, MySQL uses 0-based. Out-of-bounds and NULL indices return null. Adds 9 tests covering both dialects, bounds checking, string elements, null index, and malformed nodes. Closes #19.
1 parent 31e4809 commit bdf10fc

2 files changed

Lines changed: 135 additions & 4 deletions

File tree

include/sql_engine/expression_eval.h

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -685,10 +685,31 @@ Value evaluate_expression(const AstNode* expr,
685685
}
686686
return value_null(); // no executor or no parsed child
687687
}
688-
case NodeType::NODE_TUPLE: return value_null(); // requires row/tuple value type
689-
case NodeType::NODE_ARRAY_CONSTRUCTOR: return value_null(); // requires array value type
690-
case NodeType::NODE_ARRAY_SUBSCRIPT: return value_null(); // requires array support
691-
case NodeType::NODE_FIELD_ACCESS: return value_null(); // requires composite type
688+
case NodeType::NODE_TUPLE: return value_null(); // Tuple as standalone value not supported
689+
case NodeType::NODE_ARRAY_CONSTRUCTOR: return value_null(); // Bare array without subscript
690+
case NodeType::NODE_ARRAY_SUBSCRIPT: {
691+
const AstNode* array_expr = expr->first_child;
692+
const AstNode* index_expr = array_expr ? array_expr->next_sibling : nullptr;
693+
if (!array_expr || !index_expr) return value_null();
694+
695+
Value idx = evaluate_expression<D>(index_expr, resolve, functions, arena, subquery_exec);
696+
if (idx.is_null()) return value_null();
697+
int64_t i = idx.to_int64();
698+
699+
if (array_expr->type == NodeType::NODE_ARRAY_CONSTRUCTOR) {
700+
// 1-based indexing for PostgreSQL, 0-based for MySQL
701+
int64_t pos = (D == sql_parser::Dialect::PostgreSQL) ? i - 1 : i;
702+
if (pos < 0) return value_null();
703+
const AstNode* child = array_expr->first_child;
704+
for (int64_t c = 0; c < pos && child; ++c) {
705+
child = child->next_sibling;
706+
}
707+
if (!child) return value_null();
708+
return evaluate_expression<D>(child, resolve, functions, arena, subquery_exec);
709+
}
710+
return value_null(); // Non-literal arrays not yet supported
711+
}
712+
case NodeType::NODE_FIELD_ACCESS: return value_null(); // Composite field access requires composite types
692713

693714
default:
694715
return value_null(); // unknown node type

tests/test_expression_eval.cpp

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,3 +698,113 @@ TEST_F(ExprEvalTest, ArrayConstructorReturnsNull) {
698698
AstNode* n = make_node(arena, NodeType::NODE_ARRAY_CONSTRUCTOR);
699699
EXPECT_TRUE(eval_mysql(n).is_null());
700700
}
701+
702+
// ===== Array Subscript Evaluation =====
703+
704+
TEST_F(ExprEvalTest, ArraySubscriptPg1Based) {
705+
// ARRAY[10, 20, 30][2] with PostgreSQL -> 20 (1-based)
706+
AstNode* arr = make_node(arena, NodeType::NODE_ARRAY_CONSTRUCTOR);
707+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "10"));
708+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "20"));
709+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "30"));
710+
711+
AstNode* subscript = make_node(arena, NodeType::NODE_ARRAY_SUBSCRIPT);
712+
subscript->add_child(arr);
713+
subscript->add_child(leaf(NodeType::NODE_LITERAL_INT, "2"));
714+
715+
auto v = eval_pg(subscript);
716+
EXPECT_EQ(v.tag, Value::TAG_INT64);
717+
EXPECT_EQ(v.int_val, 20);
718+
}
719+
720+
TEST_F(ExprEvalTest, ArraySubscriptPgFirstElement) {
721+
// ARRAY[10, 20, 30][1] with PostgreSQL -> 10
722+
AstNode* arr = make_node(arena, NodeType::NODE_ARRAY_CONSTRUCTOR);
723+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "10"));
724+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "20"));
725+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "30"));
726+
727+
AstNode* subscript = make_node(arena, NodeType::NODE_ARRAY_SUBSCRIPT);
728+
subscript->add_child(arr);
729+
subscript->add_child(leaf(NodeType::NODE_LITERAL_INT, "1"));
730+
731+
auto v = eval_pg(subscript);
732+
EXPECT_EQ(v.tag, Value::TAG_INT64);
733+
EXPECT_EQ(v.int_val, 10);
734+
}
735+
736+
TEST_F(ExprEvalTest, ArraySubscriptOutOfBounds) {
737+
// ARRAY[10, 20][5] -> null
738+
AstNode* arr = make_node(arena, NodeType::NODE_ARRAY_CONSTRUCTOR);
739+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "10"));
740+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "20"));
741+
742+
AstNode* subscript = make_node(arena, NodeType::NODE_ARRAY_SUBSCRIPT);
743+
subscript->add_child(arr);
744+
subscript->add_child(leaf(NodeType::NODE_LITERAL_INT, "5"));
745+
746+
EXPECT_TRUE(eval_pg(subscript).is_null());
747+
}
748+
749+
TEST_F(ExprEvalTest, ArraySubscriptPgZeroOutOfBounds) {
750+
// ARRAY[10, 20][0] with PostgreSQL -> null (1-based, so 0 is out of bounds)
751+
AstNode* arr = make_node(arena, NodeType::NODE_ARRAY_CONSTRUCTOR);
752+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "10"));
753+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "20"));
754+
755+
AstNode* subscript = make_node(arena, NodeType::NODE_ARRAY_SUBSCRIPT);
756+
subscript->add_child(arr);
757+
subscript->add_child(leaf(NodeType::NODE_LITERAL_INT, "0"));
758+
759+
EXPECT_TRUE(eval_pg(subscript).is_null());
760+
}
761+
762+
TEST_F(ExprEvalTest, ArraySubscriptPgStringElements) {
763+
// ARRAY['hello', 'world'][1] with PostgreSQL -> 'hello'
764+
AstNode* arr = make_node(arena, NodeType::NODE_ARRAY_CONSTRUCTOR);
765+
arr->add_child(leaf(NodeType::NODE_LITERAL_STRING, "hello"));
766+
arr->add_child(leaf(NodeType::NODE_LITERAL_STRING, "world"));
767+
768+
AstNode* subscript = make_node(arena, NodeType::NODE_ARRAY_SUBSCRIPT);
769+
subscript->add_child(arr);
770+
subscript->add_child(leaf(NodeType::NODE_LITERAL_INT, "1"));
771+
772+
auto v = eval_pg(subscript);
773+
EXPECT_EQ(v.tag, Value::TAG_STRING);
774+
EXPECT_EQ(std::string(v.str_val.ptr, v.str_val.len), "hello");
775+
}
776+
777+
TEST_F(ExprEvalTest, ArraySubscriptNullIndex) {
778+
// ARRAY[10, 20][NULL] -> null
779+
AstNode* arr = make_node(arena, NodeType::NODE_ARRAY_CONSTRUCTOR);
780+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "10"));
781+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "20"));
782+
783+
AstNode* subscript = make_node(arena, NodeType::NODE_ARRAY_SUBSCRIPT);
784+
subscript->add_child(arr);
785+
subscript->add_child(make_node(arena, NodeType::NODE_LITERAL_NULL));
786+
787+
EXPECT_TRUE(eval_pg(subscript).is_null());
788+
}
789+
790+
TEST_F(ExprEvalTest, ArraySubscriptNoChildren) {
791+
// Malformed: no children -> null
792+
AstNode* subscript = make_node(arena, NodeType::NODE_ARRAY_SUBSCRIPT);
793+
EXPECT_TRUE(eval_pg(subscript).is_null());
794+
}
795+
796+
TEST_F(ExprEvalTest, ArraySubscriptMysql0Based) {
797+
// MySQL uses 0-based indexing: ARRAY[10, 20, 30][0] -> 10
798+
AstNode* arr = make_node(arena, NodeType::NODE_ARRAY_CONSTRUCTOR);
799+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "10"));
800+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "20"));
801+
arr->add_child(leaf(NodeType::NODE_LITERAL_INT, "30"));
802+
803+
AstNode* subscript = make_node(arena, NodeType::NODE_ARRAY_SUBSCRIPT);
804+
subscript->add_child(arr);
805+
subscript->add_child(leaf(NodeType::NODE_LITERAL_INT, "0"));
806+
807+
auto v = eval_mysql(subscript);
808+
EXPECT_EQ(v.tag, Value::TAG_INT64);
809+
EXPECT_EQ(v.int_val, 10);
810+
}

0 commit comments

Comments
 (0)