diff --git a/apps/tundra_shell.cpp b/apps/tundra_shell.cpp index a13de13..2dfd639 100644 --- a/apps/tundra_shell.cpp +++ b/apps/tundra_shell.cpp @@ -28,11 +28,11 @@ #include "TundraQLParser.h" #include "arrow/map_union_types.hpp" #include "common/constants.hpp" -#include "main/database.hpp" -#include "linenoise.h" #include "common/logger.hpp" #include "common/types.hpp" #include "common/utils.hpp" +#include "linenoise.h" +#include "main/database.hpp" // Tee stream class that outputs to both console and file class TeeStream : public std::ostream { @@ -525,7 +525,7 @@ class TundraQLVisitorImpl : public tundraql::TundraQLBaseVisitor { node_type = node_alias; } - auto query_builder = tundradb::Query::from(node_alias + ":" + node_type); + auto query_builder = tundradb::Query::match(node_alias + ":" + node_type); for (size_t i = 0; i < edges.size(); i++) { auto edge = edges[i]; @@ -807,7 +807,7 @@ class TundraQLVisitorImpl : public tundraql::TundraQLBaseVisitor { schema_name = alias; } - auto query_builder = tundradb::Query::from(alias + ":" + schema_name); + auto query_builder = tundradb::Query::match(alias + ":" + schema_name); if (ctx->whereClause()) { processWhereClause(query_builder, ctx->whereClause()); @@ -1082,7 +1082,7 @@ class TundraQLVisitorImpl : public tundraql::TundraQLBaseVisitor { return qb; } // Mode 2: single nodePattern → build a trivial query - return tundradb::Query::from(alias + ":" + schema_name); + return tundradb::Query::match(alias + ":" + schema_name); }(); // Build alias→schema map from the query builder's pattern @@ -1456,7 +1456,7 @@ class TundraQLVisitorImpl : public tundraql::TundraQLBaseVisitor { try { // Build a query to find matching nodes - auto query_builder = tundradb::Query::from("n:" + node_type); + auto query_builder = tundradb::Query::match("n:" + node_type); // Add WHERE conditions for each property for (const auto& [prop_name, prop_value] : properties) { @@ -2211,4 +2211,4 @@ int main(int argc, char* argv[]) { g_tee_stream.reset(); return 0; -} \ No newline at end of file +} diff --git a/bench/tundra_runner.cpp b/bench/tundra_runner.cpp index a52e002..f0b78d2 100644 --- a/bench/tundra_runner.cpp +++ b/bench/tundra_runner.cpp @@ -1,44 +1,45 @@ #include #include #include + #include #include #include #include +#include "common/types.hpp" #include "main/database.hpp" #include "query/query.hpp" -#include "common/types.hpp" using namespace tundradb; -static arrow::Result> read_csv(const std::string& path) { +static arrow::Result> read_csv( + const std::string& path) { ARROW_ASSIGN_OR_RAISE(auto input, arrow::io::ReadableFile::Open(path)); auto read_options = arrow::csv::ReadOptions::Defaults(); auto parse_options = arrow::csv::ParseOptions::Defaults(); auto convert_options = arrow::csv::ConvertOptions::Defaults(); - ARROW_ASSIGN_OR_RAISE(auto reader, - arrow::csv::TableReader::Make(arrow::io::default_io_context(), input, - read_options, parse_options, convert_options)); + ARROW_ASSIGN_OR_RAISE( + auto reader, arrow::csv::TableReader::Make( + arrow::io::default_io_context(), input, read_options, + parse_options, convert_options)); return reader->Read(); } -void load_data(Database& db, const std::string& users_csv, - const std::string& companies_csv, - const std::string& friend_csv, +void load_data(Database& db, const std::string& users_csv, + const std::string& companies_csv, const std::string& friend_csv, const std::string& works_at_csv) { auto load_start = std::chrono::high_resolution_clock::now(); // Define schemas (must include "id" field) - auto user_schema = arrow::schema({ - arrow::field("name", arrow::utf8()), + auto user_schema = arrow::schema({arrow::field("name", arrow::utf8()), arrow::field("age", arrow::int64()), arrow::field("country", arrow::utf8())}); db.get_schema_registry()->create("User", user_schema).ValueOrDie(); - auto company_schema = arrow::schema({ - arrow::field("name", arrow::utf8()), - arrow::field("industry", arrow::utf8())}); + auto company_schema = + arrow::schema({arrow::field("name", arrow::utf8()), + arrow::field("industry", arrow::utf8())}); db.get_schema_registry()->create("Company", company_schema).ValueOrDie(); auto users_tbl = read_csv(users_csv).ValueOrDie(); users_tbl = users_tbl->CombineChunks().ValueOrDie(); @@ -58,9 +59,12 @@ void load_data(Database& db, const std::string& users_csv, auto name_idx = users_tbl->schema()->GetFieldIndex("name"); auto age_idx = users_tbl->schema()->GetFieldIndex("age"); auto country_idx = users_tbl->schema()->GetFieldIndex("country"); - auto name_arr = std::static_pointer_cast(users_tbl->column(name_idx)->chunk(0)); - auto age_arr = std::static_pointer_cast(users_tbl->column(age_idx)->chunk(0)); - auto country_arr = std::static_pointer_cast(users_tbl->column(country_idx)->chunk(0)); + auto name_arr = std::static_pointer_cast( + users_tbl->column(name_idx)->chunk(0)); + auto age_arr = std::static_pointer_cast( + users_tbl->column(age_idx)->chunk(0)); + auto country_arr = std::static_pointer_cast( + users_tbl->column(country_idx)->chunk(0)); for (int64_t i = 0; i < users_tbl->num_rows(); ++i) { std::unordered_map data; data["name"] = Value(std::string(name_arr->GetView(i))); @@ -73,11 +77,12 @@ void load_data(Database& db, const std::string& users_csv, // Load Companies (global ids continue after users) - auto cname_idx = companies_tbl->schema()->GetFieldIndex("name"); auto ind_idx = companies_tbl->schema()->GetFieldIndex("industry"); - auto cname_arr = std::static_pointer_cast(companies_tbl->column(cname_idx)->chunk(0)); - auto ind_arr = std::static_pointer_cast(companies_tbl->column(ind_idx)->chunk(0)); + auto cname_arr = std::static_pointer_cast( + companies_tbl->column(cname_idx)->chunk(0)); + auto ind_arr = std::static_pointer_cast( + companies_tbl->column(ind_idx)->chunk(0)); for (int64_t i = 0; i < companies_tbl->num_rows(); ++i) { std::unordered_map data; data["name"] = Value(std::string(cname_arr->GetView(i))); @@ -89,41 +94,47 @@ void load_data(Database& db, const std::string& users_csv, auto fsrc_idx = friend_tbl->schema()->GetFieldIndex("src"); auto fdst_idx = friend_tbl->schema()->GetFieldIndex("dst"); - auto fsrc = std::static_pointer_cast(friend_tbl->column(fsrc_idx)->chunk(0)); - auto fdst = std::static_pointer_cast(friend_tbl->column(fdst_idx)->chunk(0)); + auto fsrc = std::static_pointer_cast( + friend_tbl->column(fsrc_idx)->chunk(0)); + auto fdst = std::static_pointer_cast( + friend_tbl->column(fdst_idx)->chunk(0)); for (int64_t i = 0; i < friend_tbl->num_rows(); ++i) { db.connect(fsrc->Value(i), "FRIEND", fdst->Value(i)).ValueOrDie(); } - auto wsrc_idx = works_tbl->schema()->GetFieldIndex("src"); auto wdst_idx = works_tbl->schema()->GetFieldIndex("dst"); - auto wsrc = std::static_pointer_cast(works_tbl->column(wsrc_idx)->chunk(0)); - auto wdst = std::static_pointer_cast(works_tbl->column(wdst_idx)->chunk(0)); + auto wsrc = std::static_pointer_cast( + works_tbl->column(wsrc_idx)->chunk(0)); + auto wdst = std::static_pointer_cast( + works_tbl->column(wdst_idx)->chunk(0)); for (int64_t i = 0; i < works_tbl->num_rows(); ++i) { - db.connect(wsrc->Value(i), "WORKS_AT", users_count + wdst->Value(i)).ValueOrDie(); + db.connect(wsrc->Value(i), "WORKS_AT", users_count + wdst->Value(i)) + .ValueOrDie(); } db.get_table("User", nullptr).ValueOrDie(); db.get_table("Company", nullptr).ValueOrDie(); - + auto load_end = std::chrono::high_resolution_clock::now(); auto load_duration = std::chrono::duration_cast( load_end - load_start); - std::cerr << "Data load time: " << load_duration.count() << " ms" << std::endl; + std::cerr << "Data load time: " << load_duration.count() << " ms" + << std::endl; } int64_t run_query(Database& db) { auto query_start_time = std::chrono::high_resolution_clock::now(); - Query query = Query::from("u:User") - .where("u.age", CompareOp::Gt, Value(30)) - .and_where("u.country", CompareOp::Eq, Value(std::string("US"))) - .traverse("u", "FRIEND", "f:User", TraverseType::Inner) - .where("f.age", CompareOp::Gt, Value((int64_t)25)) - .select() - .parallel(true) - .inline_where() - .build(); + Query query = + Query::match("u:User") + .where("u.age", CompareOp::Gt, Value(30)) + .and_where("u.country", CompareOp::Eq, Value(std::string("US"))) + .traverse("u", "FRIEND", "f:User", TraverseType::Inner) + .where("f.age", CompareOp::Gt, Value((int64_t)25)) + .select() + .parallel(true) + .inline_where() + .build(); auto res = db.query(query); auto query_end_time = std::chrono::high_resolution_clock::now(); @@ -134,22 +145,24 @@ int64_t run_query(Database& db) { std::cerr << "Query failed: " << res.status().ToString() << "\n"; return -1; } - + auto table = res.ValueOrDie()->table(); int64_t row_count = table ? table->num_rows() : 0; - + // Output in machine-readable format for Python parser std::cout << query_duration.count() << std::endl; // Just the time in ms - + return row_count; } int main(int argc, char** argv) { if (argc < 5) { - std::cerr << "Usage: " << argv[0] << " [repetitions]\n"; + std::cerr << "Usage: " << argv[0] + << " " + "[repetitions]\n"; return 1; } - + std::string users_csv = argv[1]; std::string companies_csv = argv[2]; std::string friend_csv = argv[3]; @@ -158,16 +171,16 @@ int main(int argc, char** argv) { // Build in-memory DB auto config = make_config() - .with_persistence_enabled(false) - .with_shard_capacity(200000) - .with_chunk_size(100000) - .build(); + .with_persistence_enabled(false) + .with_shard_capacity(200000) + .with_chunk_size(100000) + .build(); Database db(config); - + // Load data once (not timed for benchmark) load_data(db, users_csv, companies_csv, friend_csv, works_at_csv); - + // Run query multiple times and output each timing int64_t rows = 0; for (int i = 0; i < repetitions; i++) { @@ -177,7 +190,7 @@ int main(int argc, char** argv) { return 2; } } - + std::cerr << "rows=" << rows << std::endl; return 0; -} \ No newline at end of file +} diff --git a/include/main/database.hpp b/include/main/database.hpp index bb65393..ae37591 100644 --- a/include/main/database.hpp +++ b/include/main/database.hpp @@ -150,7 +150,7 @@ class Database { * * Mode 2 - by MATCH query (alias-qualified SET, multi-schema): * db.update(UpdateQuery::match( - * Query::from("u:User") + * Query::match("u:User") * .traverse("u", "WORKS_AT", "c:Company") * .where("c.name", CompareOp::Eq, Value("Google")) * .build() @@ -177,13 +177,13 @@ class Database { const std::vector &fields, UpdateType update_type, UpdateResult &result); - /** Initialize QueryState from query: temporal context, FROM table, prepare. + /** Initialize QueryState from query: temporal context, root table, prepare. */ [[nodiscard]] arrow::Status init_query_state(const Query &query, QueryState &query_state) const; - /** Inline WHERE clauses applicable to the FROM alias. */ - [[nodiscard]] arrow::Status inline_from_where(const Query &query, + /** Inline WHERE clauses applicable to the root alias. */ + [[nodiscard]] arrow::Status inline_root_where(const Query &query, QueryState &query_state, QueryResult &result) const; @@ -196,12 +196,30 @@ class Database { /** Execute a single TRAVERSE clause, updating query_state in-place. */ [[nodiscard]] arrow::Status execute_traverse( const std::shared_ptr &traverse, QueryState &query_state, - const Query &query, size_t clause_index, QueryResult &result) const; + const Query &query, size_t clause_index, size_t traverse_index, + QueryResult &result) const; - /** Apply a single-variable WHERE filter, or defer to post_where. */ - [[nodiscard]] arrow::Status apply_where_filter( - const std::shared_ptr &where, QueryState &query_state, - std::vector> &post_where) const; + /** High-level action chosen for one WHERE clause in legacy execution mode. */ + struct WhereDisposition { + enum class Kind { + Skip, + Defer, + ApplyToAlias, + }; + + Kind kind = Kind::Skip; + std::string alias; + }; + + /** Classify a WHERE clause as skipped, deferred, or directly applicable. */ + [[nodiscard]] arrow::Result classify_where_filter( + const std::shared_ptr &where, + const QueryState &query_state) const; + + /** Apply a single-alias WHERE clause to an already materialized alias. */ + [[nodiscard]] arrow::Status apply_alias_where( + const std::shared_ptr &where, const std::string &alias, + QueryState &query_state) const; /** Build the final output table: denormalize, populate rows, apply * deferred WHERE filters, and project via SELECT. */ diff --git a/include/query/execution.hpp b/include/query/execution.hpp index f26d363..8c4d826 100644 --- a/include/query/execution.hpp +++ b/include/query/execution.hpp @@ -19,6 +19,7 @@ #include #include "query/query.hpp" +#include "query/where_planner.hpp" #include "schema/schema.hpp" namespace tundradb { @@ -448,8 +449,9 @@ struct QueryState { /// Arrow tables keyed by schema alias. std::unordered_map> tables; - SchemaRef from; ///< Source schema from the FROM clause. + SchemaRef root; ///< Root schema for query execution. std::vector traversals; ///< Traverse clauses in query order. + std::optional where_plan; ///< Planned WHERE execution. std::shared_ptr schema_registry; ///< Node schema registry. std::shared_ptr node_manager; ///< Node storage. @@ -724,7 +726,8 @@ std::vector> get_where_to_inline( arrow::Result> inline_where( const SchemaRef& ref, std::shared_ptr table, QueryState& query_state, - const std::vector>& where_exprs); + const std::vector>& where_exprs, + bool mark_inlined = true); /** * @brief Prepares a query for execution: registers aliases, resolves fields, diff --git a/include/query/query.hpp b/include/query/query.hpp index db7bcae..867d941 100644 --- a/include/query/query.hpp +++ b/include/query/query.hpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -244,7 +245,9 @@ class WhereExpr { get_conditions_for_variable(const std::string& variable) const = 0; /** @brief Returns the set of all variables referenced in this expression. */ - virtual std::set get_all_variables() const = 0; + virtual const std::set& get_all_variables() const = 0; + + size_t get_vars_count() const { return get_all_variables().size(); } /** @brief Returns the first variable name found (useful for single-var * conditions). */ @@ -253,6 +256,9 @@ class WhereExpr { /** @brief Returns true if this expression can be inlined for the given * variable. */ virtual bool can_inline(const std::string& variable) const = 0; + + protected: + mutable std::set vars_; }; /** @brief The type of graph traversal / join to perform. */ @@ -361,7 +367,7 @@ class ComparisonExpr : public Clause, public WhereExpr { std::string extract_first_variable() const override; - std::set get_all_variables() const override; + const std::set& get_all_variables() const override; arrow::Result resolve_field_ref( const std::function>( @@ -422,7 +428,7 @@ class LogicalExpr : public Clause, public WhereExpr { friend std::ostream& operator<<(std::ostream& os, const LogicalExpr& expr); - std::set get_all_variables() const override; + const std::set& get_all_variables() const override; bool can_inline(const std::string& variable) const override; }; @@ -463,12 +469,12 @@ struct ExecutionConfig { /** * @brief Immutable query descriptor built via Query::Builder. * - * Contains the FROM schema, a list of clauses (TRAVERSE, WHERE, SELECT), - * execution configuration, and optional temporal snapshot. + * Contains the initial MATCH binding, a list of clauses (TRAVERSE, WHERE, + * SELECT), execution configuration, and optional temporal snapshot. */ class Query { private: - SchemaRef from_; + SchemaRef root_; std::vector> clauses_; std::shared_ptr select, bool optimize_where, ExecutionConfig execution_config, std::optional temporal_snapshot = std::nullopt) - : from_(std::move(from)), + : root_(std::move(root)), clauses_(std::move(clauses)), select_(std::move(select)), inline_where_(optimize_where), @@ -488,7 +494,7 @@ class Query { temporal_snapshot_(std::move(temporal_snapshot)) {} class Builder; - [[nodiscard]] const SchemaRef& from() const { return from_; } + [[nodiscard]] const SchemaRef& root() const { return root_; } [[nodiscard]] const std::vector>& clauses() const { return clauses_; } @@ -518,12 +524,13 @@ class Query { return nullptr; } - static Builder from(const std::string& schema) { return Builder(schema); } + /** @brief Begin a MATCH query from the initial bound schema alias. */ + static Builder match(const std::string& schema) { return Builder(schema); } /** @brief Fluent builder for constructing Query objects. */ class Builder { private: - SchemaRef from_; + SchemaRef root_; std::vector> clauses_; std::shared_ptr(std::vector( id_column_set.begin(), id_column_set.end())), match_query.inline_where(), match_query.execution_config(), diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 70d70af..d242756 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -104,6 +104,10 @@ add_executable(join_test join_test.cpp ) +add_executable(join_where_test + join_where_test.cpp +) + # Add benchmark test add_executable(benchmark_test benchmark_test.cpp @@ -116,6 +120,9 @@ add_executable(where_pushdown_join_test add_executable(where_expression_test where_expression_test.cpp) +add_executable(where_planner_test + where_planner_test.cpp) + add_executable(memory_arena_test memory_arena_test.cpp) @@ -288,6 +295,18 @@ target_link_libraries(join_test LLVMSupport LLVMCore ) +target_link_libraries(join_where_test + PRIVATE + core + Arrow::arrow_shared + Parquet::parquet_shared + GTest::GTest + GTest::Main + pthread + TBB::tbb + LLVMSupport LLVMCore +) + # Link benchmark test with Google Benchmark and other dependencies target_link_libraries(benchmark_test PRIVATE @@ -327,6 +346,17 @@ target_link_libraries(where_expression_test LLVMSupport LLVMCore ) +target_link_libraries(where_planner_test + PRIVATE + core + Arrow::arrow_shared + GTest::GTest + GTest::Main + pthread + TBB::tbb + LLVMSupport LLVMCore +) + target_link_libraries(memory_arena_test PRIVATE GTest::GTest @@ -539,9 +569,15 @@ if(ENABLE_SANITIZERS) target_compile_options(join_test PRIVATE ${SANITIZER_COMPILE_FLAGS}) target_link_options(join_test PRIVATE ${SANITIZER_LINK_FLAGS}) + + target_compile_options(join_where_test PRIVATE ${SANITIZER_COMPILE_FLAGS}) + target_link_options(join_where_test PRIVATE ${SANITIZER_LINK_FLAGS}) target_compile_options(where_expression_test PRIVATE ${SANITIZER_COMPILE_FLAGS}) target_link_options(where_expression_test PRIVATE ${SANITIZER_LINK_FLAGS}) + + target_compile_options(where_planner_test PRIVATE ${SANITIZER_COMPILE_FLAGS}) + target_link_options(where_planner_test PRIVATE ${SANITIZER_LINK_FLAGS}) target_compile_options(free_list_arena_test PRIVATE ${SANITIZER_COMPILE_FLAGS}) target_link_options(free_list_arena_test PRIVATE ${SANITIZER_LINK_FLAGS}) @@ -587,7 +623,9 @@ add_test(NAME TableInfoTest COMMAND table_info_test) add_test(NAME SchemaUtilsTest COMMAND schema_utils_test) add_test(NAME DatabaseTest COMMAND database_test) add_test(NAME JoinTest COMMAND join_test) +add_test(NAME JoinWhereTest COMMAND join_where_test) add_test(NAME WhereExpressionTest COMMAND where_expression_test) +add_test(NAME WherePlannerTest COMMAND where_planner_test) add_test(NAME MemoryArenaTest COMMAND memory_arena_test) add_test(NAME FreeListArenaTest COMMAND free_list_arena_test) add_test(NAME NodeArenaTest COMMAND node_arena_test) @@ -626,6 +664,7 @@ set_tests_properties( SchemaUtilsTest DatabaseTest JoinTest + JoinWhereTest WhereExpressionTest FreeListArenaTest UpdateQueryTest @@ -633,4 +672,4 @@ set_tests_properties( TypeSystemTest PROPERTIES ISOLATED TRUE # This ensures tests run in isolation -) \ No newline at end of file +) diff --git a/tests/array_query_test.cpp b/tests/array_query_test.cpp index 3369536..3144d76 100644 --- a/tests/array_query_test.cpp +++ b/tests/array_query_test.cpp @@ -79,7 +79,7 @@ class ArrayQueryTest : public ::testing::Test { /// Query the "Item" table and return the full Arrow table. std::shared_ptr query_items() { - auto query = Query::from("i:Item").build(); + auto query = Query::match("i:Item").build(); auto result = db_->query(query).ValueOrDie(); return result->table(); } @@ -336,7 +336,7 @@ TEST_F(ArrayQueryTest, SequentialArrayUpdatesAccumulate) { } TEST_F(ArrayQueryTest, UpdateByMatchSetsArray) { - auto q = Query::from("i:Item") + auto q = Query::match("i:Item") .where("i.name", CompareOp::Eq, Value("Bob"s)) .build(); std::vector new_tags = {Value{"matched"s}}; @@ -488,7 +488,7 @@ TEST_F(ArrayQueryTest, AppendEmptyVectorIsNoop) { TEST_F(ArrayQueryTest, AppendByMatchQuery) { // Append "matched" to all Items where name = "Bob" - auto q = Query::from("i:Item") + auto q = Query::match("i:Item") .where("i.name", CompareOp::Eq, Value("Bob"s)) .build(); std::vector to_append = {Value{"matched"s}}; @@ -569,12 +569,12 @@ class VersionedArrayTest : public ::testing::Test { } std::shared_ptr query_items() { - auto query = Query::from("i:Item").build(); + auto query = Query::match("i:Item").build(); return db_->query(query).ValueOrDie()->table(); } std::shared_ptr query_items_as_of(uint64_t valid_time) { - auto query = Query::from("i:Item").as_of_valid_time(valid_time).build(); + auto query = Query::match("i:Item").as_of_valid_time(valid_time).build(); return db_->query(query).ValueOrDie()->table(); } diff --git a/tests/benchmark_test.cpp b/tests/benchmark_test.cpp index af04f74..732e9af 100644 --- a/tests/benchmark_test.cpp +++ b/tests/benchmark_test.cpp @@ -242,7 +242,7 @@ void BM_FullScan(::benchmark::State& state) { fixture->createUsers(node_count); for (auto _ : state) { - Query query = Query::from("u:User").build(); + Query query = Query::match("u:User").build(); auto result = fixture->db()->query(query); if (!result.ok() || result.ValueOrDie()->table()->num_rows() != node_count) { @@ -268,7 +268,7 @@ void BM_SimpleJoin(::benchmark::State& state) { for (auto _ : state) { Query query = - Query::from("u:User") + Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner) .build(); auto result = fixture->db()->query(query); @@ -308,7 +308,7 @@ void BM_ComplexJoin(::benchmark::State& state) { for (auto _ : state) { // Complex 3-way join: Users -> Friends -> Companies Query query = - Query::from("u:User") + Query::match("u:User") .traverse("u", "FRIEND", "f:User", TraverseType::Inner) .traverse("f", "WORKS_AT", "c:Company", TraverseType::Inner) .build(); @@ -329,7 +329,7 @@ void BM_FilteredQuery(::benchmark::State& state) { for (auto _ : state) { // Query with WHERE clause - users over 50 Query query = - Query::from("u:User").where("u.age", CompareOp::Gt, Value(50)).build(); + Query::match("u:User").where("u.age", CompareOp::Gt, Value(50)).build(); auto result = fixture->db()->query(query); if (!result.ok()) { state.SkipWithError("Filtered query failed"); @@ -341,14 +341,14 @@ void BM_FilteredQuery(::benchmark::State& state) { // Google Test cases for correctness verification TEST_F(SmallDatasetTest, NodeCreationCorrectness) { - Query query = Query::from("u:User").build(); + Query query = Query::match("u:User").build(); auto result = fixture->db()->query(query); ASSERT_TRUE(result.ok()); EXPECT_EQ(result.ValueOrDie()->table()->num_rows(), 100); } TEST_F(SmallDatasetTest, SimpleJoinCorrectness) { - Query query = Query::from("u:User") + Query query = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner) .build(); auto result = fixture->db()->query(query); @@ -366,7 +366,7 @@ TEST_F(SmallDatasetTest, SimpleJoinCorrectness) { } TEST_F(SmallDatasetTest, ComplexJoinCorrectness) { - Query query = Query::from("u:User") + Query query = Query::match("u:User") .traverse("u", "FRIEND", "f:User", TraverseType::Inner) .traverse("f", "WORKS_AT", "c:Company", TraverseType::Inner) .build(); @@ -380,7 +380,7 @@ TEST_F(SmallDatasetTest, ComplexJoinCorrectness) { TEST_F(SmallDatasetTest, FilteredQueryCorrectness) { Query query = - Query::from("u:User").where("u.age", CompareOp::Gt, Value(50)).build(); + Query::match("u:User").where("u.age", CompareOp::Gt, Value(50)).build(); auto result = fixture->db()->query(query); ASSERT_TRUE(result.ok()); @@ -404,7 +404,7 @@ TEST_F(SmallDatasetTest, FilteredQueryCorrectness) { TEST_F(MediumDatasetTest, ScalabilityTest) { auto start_time = std::chrono::high_resolution_clock::now(); - Query query = Query::from("u:User") + Query query = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner) .build(); auto result = fixture->db()->query(query); @@ -429,7 +429,7 @@ TEST_F(LargeDatasetTest, PerformanceBaseline) { auto start = std::chrono::high_resolution_clock::now(); Query query = - Query::from("u:User") + Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner) //.parallel(true) //.parallel_thread_count(4) diff --git a/tests/join_test.cpp b/tests/join_test.cpp index 5df8a24..e32340d 100644 --- a/tests/join_test.cpp +++ b/tests/join_test.cpp @@ -96,7 +96,7 @@ std::shared_ptr setup_test_db() { TEST(JoinTest, MatchAll) { auto db = setup_test_db(); - Query query = Query::from("u:users").build(); + Query query = Query::match("u:users").build(); auto query_result = db->query(query); ASSERT_TRUE(query_result.ok()); @@ -116,7 +116,7 @@ TEST(JoinTest, UserFriendCompanyInnerJoin) { db->connect(1, "works-at", 1).ValueOrDie(); Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Inner) .traverse("f", "works-at", "c:companies", TraverseType::Inner) .build(); @@ -179,7 +179,7 @@ TEST(JoinTest, JoinFromSameNode) { db->connect(0, "friend", 1).ValueOrDie(); // alex -> bob db->connect(0, "friend", 2).ValueOrDie(); // alex -> jeff - Query query = Query::from("u:users") + Query query = Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Inner) .build(); @@ -252,7 +252,7 @@ TEST(JoinTest, InnerJoinFromSameNodeMultiTarget) { db->connect(0, "works-at", 1).ValueOrDie(); // alex -> google Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Inner) .traverse("u", "works-at", "c:companies", TraverseType::Inner) .build(); @@ -340,7 +340,7 @@ TEST(JoinTest, InnerJoinFromSameNodeAndEndConnections) { db->connect(2, "works-at", 2).ValueOrDie(); // jeff -> aws Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Inner) .traverse("u", "works-at", "c:companies", TraverseType::Inner) .build(); @@ -430,7 +430,7 @@ TEST(JoinTest, EmptyResultFromInnerJoin) { // Query that will return no results because jeff doesn't work anywhere Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f1:users", TraverseType::Inner) .traverse("f1", "friend", "f2:users", TraverseType::Inner) .traverse("f2", "works-at", "c:companies", TraverseType::Inner) @@ -465,7 +465,7 @@ TEST(JoinTest, MultiPathToSameTarget) { // Query: Find all friends of alex who work at the same company as alex Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Inner) .traverse("u", "works-at", "c1:companies", TraverseType::Inner) .traverse("f", "works-at", "c2:companies", TraverseType::Inner) @@ -474,6 +474,7 @@ TEST(JoinTest, MultiPathToSameTarget) { .where( "c2.id", CompareOp::Eq, Value((int64_t)0)) // Filter for friend's company (also IBM ID 0) + .inline_where() .build(); auto query_result = db->query(query); @@ -540,7 +541,7 @@ TEST(JoinTest, CartesianProductExplosion) { // Query: Friends of alex and where they work // Results in 3 friends × ~2 companies each = ~6 rows total Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Inner) .traverse("f", "works-at", "c:companies", TraverseType::Inner) .build(); @@ -588,7 +589,7 @@ TEST(JoinTest, LeftJoin) { // LEFT JOIN: Keep all users even if they don't work at any company Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Inner) .traverse("f", "works-at", "c:companies", TraverseType::Left) .build(); @@ -688,6 +689,49 @@ TEST(JoinTest, LeftJoin) { << "Expected NULL for c.size in jeff's row"; } +TEST(JoinTest, LeftJoinTargetWhereFiltersFinalRows) { + auto db = setup_test_db(); + db->connect(0, "friend", 1).ValueOrDie(); // alex -> bob + db->connect(0, "friend", 2).ValueOrDie(); // alex -> jeff + db->connect(1, "works-at", 1).ValueOrDie(); // bob -> google + db->connect(2, "works-at", 0).ValueOrDie(); // jeff -> ibm + + Query query = + Query::match("u:users") + .traverse("u", "friend", "f:users", TraverseType::Inner) + .traverse("f", "works-at", "c:companies", TraverseType::Left) + .where("c.name", CompareOp::Eq, Value("google")) + .inline_where() + .build(); + + auto query_result = db->query(query); + ASSERT_TRUE(query_result.ok()); + auto result_table = query_result.ValueOrDie()->table(); + ASSERT_NE(result_table, nullptr); + ASSERT_EQ(result_table->num_rows(), 1); + + auto friend_name_col = result_table->GetColumnByName("f.name"); + auto company_name_col = result_table->GetColumnByName("c.name"); + ASSERT_NE(friend_name_col, nullptr); + ASSERT_NE(company_name_col, nullptr); + + int bob_index = -1; + for (int64_t i = 0; i < result_table->num_rows(); ++i) { + auto friend_name_scalar = std::static_pointer_cast( + friend_name_col->GetScalar(i).ValueOrDie()); + if (friend_name_scalar->view() == "bob") { + bob_index = static_cast(i); + } + } + + ASSERT_NE(bob_index, -1); + + auto bob_company = std::static_pointer_cast( + company_name_col->GetScalar(bob_index).ValueOrDie()); + ASSERT_TRUE(bob_company->is_valid); + EXPECT_EQ(bob_company->ToString(), "google"); +} + TEST(JoinTest, RightJoin) { auto db = setup_test_db(); // Create relationships where some targets don't have matching sources @@ -700,7 +744,7 @@ TEST(JoinTest, RightJoin) { // RIGHT JOIN: Keep all companies even if no users work there Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Inner) .traverse("f", "works-at", "c:companies", TraverseType::Right) .build(); @@ -751,7 +795,7 @@ TEST(JoinTest, CombinedJoinTypes) { // Query that combines INNER, LEFT and RIGHT joins Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Left) .traverse("f", "works-at", "c:companies", TraverseType::Right) .build(); @@ -885,7 +929,7 @@ TEST(JoinTest, MultiLevelLeftJoin) { // Multi-level LEFT JOINs: Keep all users at each level Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Left) .traverse("f", "works-at", "c:companies", TraverseType::Left) .traverse("f", "likes", "l:companies", TraverseType::Left) @@ -1018,7 +1062,7 @@ TEST(JoinTest, SelfJoinWithLeftJoin) { // LEFT JOIN with self: Find all management chains, including users with no // manager or subordinates Query query = - Query::from("manager:users") + Query::match("manager:users") .traverse("manager", "manages", "employee:users", TraverseType::Left) .build(); @@ -1140,7 +1184,7 @@ TEST(JoinTest, FullOuterJoin) { // FULL OUTER JOIN: Keep all records from both sides Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Full) .traverse("f", "works-at", "c:companies", TraverseType::Full) .build(); @@ -1290,7 +1334,7 @@ TEST(JoinTest, SelectClauseFiltering) { // Query with SELECT - only get user (u) and friend (f) columns Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Inner) .traverse("f", "works-at", "c:companies", TraverseType::Inner) .select({"u", "f"}) // Only select u.* and f.* columns @@ -1369,7 +1413,7 @@ TEST(JoinTest, SelectSpecificColumns) { // Query with SELECT for specific columns Query query = - Query::from("u:users") + Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Inner) .select({"u.name", "f.age"}) // Only select specific columns .build(); @@ -1533,7 +1577,7 @@ TEST(JoinTest, MultiPatternPathThroughFriends) { // Run the query: MATCH (u:User)-[:FRIEND INNER]->(f:User), (f)-[:WORKS_AT // INNER]->(c:Company) Query query_custom = - Query::from("u:User") + Query::match("u:User") .traverse("u", "FRIEND", "f:User", TraverseType::Inner) .traverse("f", "WORKS_AT", "c:Company", TraverseType::Inner) .build(); @@ -1651,7 +1695,7 @@ TEST(JoinTest, MultiPatternWithSharedVars) { db->connect(2, "WORKS_AT", 1).ValueOrDie(); // Jeff -> Google (Company ID 1) db->connect(1, "WORKS_AT", 0).ValueOrDie(); // Bob -> IBM (Company ID 0) - Query query = Query::from("u:users") + Query query = Query::match("u:users") .traverse("u", "FRIEND", "f:users") .traverse("f", "WORKS_AT", "c:companies") .traverse("u", "WORKS_AT", "c") @@ -1727,7 +1771,7 @@ TEST(JoinTest, FullJoinFriendRelationship) { db->connect(0, "friend", 1).ValueOrDie(); // alex -> bob db->connect(0, "friend", 2).ValueOrDie(); // alex -> jeff - Query query = Query::from("u:users") + Query query = Query::match("u:users") .traverse("u", "friend", "f:users", TraverseType::Full) .build(); @@ -1835,4 +1879,4 @@ int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); // Logger::get_instance().set_level(LogLevel::DEBUG); return RUN_ALL_TESTS(); -} \ No newline at end of file +} diff --git a/tests/join_where_test.cpp b/tests/join_where_test.cpp new file mode 100644 index 0000000..9fa108e --- /dev/null +++ b/tests/join_where_test.cpp @@ -0,0 +1,588 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "common/logger.hpp" +#include "main/database.hpp" +#include "query/query.hpp" +#include "storage/metadata.hpp" + +using namespace std::string_literals; +using namespace tundradb; + +namespace tundradb { + +namespace { + +std::shared_ptr create_users_schema() { + return arrow::schema({arrow::field("name", arrow::utf8()), + arrow::field("age", arrow::int64())}); +} + +std::shared_ptr create_companies_schema() { + return arrow::schema({arrow::field("name", arrow::utf8()), + arrow::field("size", arrow::int64())}); +} + +std::string scalar_to_test_string( + const std::shared_ptr& scalar) { + if (!scalar || !scalar->is_valid) { + return "NULL"; + } + return scalar->ToString(); +} + +std::string table_to_test_string(const std::shared_ptr& table) { + std::vector column_names; + column_names.reserve(table->schema()->num_fields()); + for (const auto& field : table->schema()->fields()) { + column_names.push_back(field->name()); + } + + std::vector rows; + rows.reserve(table->num_rows()); + for (int64_t row_index = 0; row_index < table->num_rows(); ++row_index) { + std::ostringstream row; + for (size_t column_index = 0; column_index < column_names.size(); + ++column_index) { + if (column_index > 0) { + row << " | "; + } + + auto column = table->GetColumnByName(column_names[column_index]); + auto scalar_result = column->GetScalar(row_index); + EXPECT_TRUE(scalar_result.ok()) << scalar_result.status().ToString(); + if (!scalar_result.ok()) { + row << ""; + continue; + } + row << scalar_to_test_string(scalar_result.ValueOrDie()); + } + rows.push_back(row.str()); + } + + std::sort(rows.begin(), rows.end()); + + std::ostringstream out; + for (size_t i = 0; i < column_names.size(); ++i) { + if (i > 0) { + out << " | "; + } + out << column_names[i]; + } + for (const auto& row : rows) { + out << '\n' << row; + } + return out.str(); +} + +void create_user(const std::shared_ptr& db, const std::string& name, + int64_t age) { + db->create_node("users", {{"name", Value{name}}, {"age", Value{age}}}) + .ValueOrDie(); +} + +void create_company(const std::shared_ptr& db, + const std::string& name, int64_t size) { + db->create_node("companies", {{"name", Value{name}}, {"size", Value{size}}}) + .ValueOrDie(); +} + +} // namespace + +class JoinWhereTest : public ::testing::Test { + protected: + void SetUp() override { + auto db_path = "join_where_test_db_" + std::to_string(now_millis()); + auto config = make_config() + .with_db_path(db_path) + .with_shard_capacity(1000) + .with_chunk_size(1000) + .build(); + + db_ = std::make_shared(config); + db_->get_schema_registry() + ->create("users", create_users_schema()) + .ValueOrDie(); + db_->get_schema_registry() + ->create("companies", create_companies_schema()) + .ValueOrDie(); + + create_user(db_, "alex", 25); // users(0) + create_user(db_, "bob", 31); // users(1) + create_user(db_, "jeff", 33); // users(2) + + create_company(db_, "google", 3000); // companies(0) + create_company(db_, "acme", 1200); // companies(1) + create_company(db_, "meta", 900); // companies(2) + + db_->connect(0, "works-at", 0).ValueOrDie(); // alex -> google + db_->connect(1, "works-at", 1).ValueOrDie(); // bob -> acme + + db_->connect(0, "friend", 1).ValueOrDie(); // alex -> bob + db_->connect(0, "friend", 2).ValueOrDie(); // alex -> jeff + } + + void expect_query_output(const Query& query, const std::string& query_text, + const std::string& expected_table) { + auto result = db_->query(query); + ASSERT_TRUE(result.ok()) << result.status().ToString(); + auto actual = table_to_test_string(result.ValueOrDie()->table()); + EXPECT_EQ(actual, expected_table) << "Query:\n" << query_text; + } + + std::shared_ptr db_; +}; + +/* +TundraQL query: +MATCH (u:users)-[:works-at INNER]->(c:companies) +WHERE c.name = "google" +SELECT u.name, c.name; + +Expected output table: +u.name | c.name +alex | google +*/ +TEST_F(JoinWhereTest, InnerJoinTargetWhereFiltersMatchedRows) { + const std::string query_text = + "MATCH (u:users)-[:works-at INNER]->(c:companies)\n" + "WHERE c.name = \"google\"\n" + "SELECT u.name, c.name;"; + const std::string expected_table = + "u.name | c.name\n" + "alex | google"; + + auto query = + Query::match("u:users") + .traverse("u", "works-at", "c:companies", TraverseType::Inner) + .where("c.name", CompareOp::Eq, Value("google"s)) + .select({"u.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:works-at INNER]->(c:companies) +WHERE u.name = "alex" +SELECT u.name, c.name; + +Expected output table: +u.name | c.name +alex | google +*/ +TEST_F(JoinWhereTest, InnerJoinSourceWhereFiltersBeforeTraverse) { + const std::string query_text = + "MATCH (u:users)-[:works-at INNER]->(c:companies)\n" + "WHERE u.name = \"alex\"\n" + "SELECT u.name, c.name;"; + const std::string expected_table = + "u.name | c.name\n" + "alex | google"; + + auto query = + Query::match("u:users") + .traverse("u", "works-at", "c:companies", TraverseType::Inner) + .where("u.name", CompareOp::Eq, Value("alex"s)) + .select({"u.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:works-at INNER]->(c:companies) +WHERE u.name = "alex" AND c.name = "google" +SELECT u.name, c.name; + +Expected output table: +u.name | c.name +alex | google +*/ +TEST_F(JoinWhereTest, InnerJoinSourceAndTargetWhereMatchesSingleRow) { + const std::string query_text = + "MATCH (u:users)-[:works-at INNER]->(c:companies)\n" + "WHERE u.name = \"alex\" AND c.name = \"google\"\n" + "SELECT u.name, c.name;"; + const std::string expected_table = + "u.name | c.name\n" + "alex | google"; + + auto query = + Query::match("u:users") + .traverse("u", "works-at", "c:companies", TraverseType::Inner) + .where("u.name", CompareOp::Eq, Value("alex"s)) + .and_where("c.name", CompareOp::Eq, Value("google"s)) + .select({"u.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:works-at LEFT]->(c:companies) +WHERE u.name = "jeff" +SELECT u.name, c.name; + +Expected output table: +u.name | c.name +jeff | NULL +*/ +TEST_F(JoinWhereTest, LeftJoinSourceWhereKeepsNullExtendedSourceRow) { + const std::string query_text = + "MATCH (u:users)-[:works-at LEFT]->(c:companies)\n" + "WHERE u.name = \"jeff\"\n" + "SELECT u.name, c.name;"; + const std::string expected_table = + "u.name | c.name\n" + "jeff | NULL"; + + auto query = Query::match("u:users") + .traverse("u", "works-at", "c:companies", TraverseType::Left) + .where("u.name", CompareOp::Eq, Value("jeff"s)) + .select({"u.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:works-at LEFT]->(c:companies) +WHERE u.name = "alex" AND c.name = "google" +SELECT u.name, c.name; + +Expected output table: +u.name | c.name +alex | google +*/ +TEST_F(JoinWhereTest, LeftJoinSourceAndTargetWhereMatchesQualifiedRow) { + const std::string query_text = + "MATCH (u:users)-[:works-at LEFT]->(c:companies)\n" + "WHERE u.name = \"alex\" AND c.name = \"google\"\n" + "SELECT u.name, c.name;"; + const std::string expected_table = + "u.name | c.name\n" + "alex | google"; + + auto query = Query::match("u:users") + .traverse("u", "works-at", "c:companies", TraverseType::Left) + .where("u.name", CompareOp::Eq, Value("alex"s)) + .and_where("c.name", CompareOp::Eq, Value("google"s)) + .select({"u.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:works-at LEFT]->(c:companies) +WHERE c.name = "google" OR u.name = "jeff" +SELECT u.name, c.name; + +Expected output table: +u.name | c.name +alex | google +jeff | NULL +*/ +TEST_F(JoinWhereTest, LeftJoinMixedAliasOrRemainsResidual) { + const std::string query_text = + "MATCH (u:users)-[:works-at LEFT]->(c:companies)\n" + "WHERE c.name = \"google\" OR u.name = \"jeff\"\n" + "SELECT u.name, c.name;"; + const std::string expected_table = + "u.name | c.name\n" + "alex | google\n" + "jeff | NULL"; + + auto query = Query::match("u:users") + .traverse("u", "works-at", "c:companies", TraverseType::Left) + .where("c.name", CompareOp::Eq, Value("google"s)) + .or_where("u.name", CompareOp::Eq, Value("jeff"s)) + .select({"u.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:works-at LEFT]->(c:companies) +WHERE c.name = "google" +SELECT u.name, c.name; + +Expected output table: +u.name | c.name +alex | google +*/ +TEST_F(JoinWhereTest, LeftJoinTargetWhereShouldFilterOutNullExtendedRows) { + const std::string query_text = + "MATCH (u:users)-[:works-at LEFT]->(c:companies)\n" + "WHERE c.name = \"google\"\n" + "SELECT u.name, c.name;"; + const std::string expected_table = + "u.name | c.name\n" + "alex | google"; + + auto query = Query::match("u:users") + .traverse("u", "works-at", "c:companies", TraverseType::Left) + .where("c.name", CompareOp::Eq, Value("google"s)) + .select({"u.name", "c.name"}) + .inline_where() + .build(); + + auto result = db_->query(query); + ASSERT_TRUE(result.ok()) << result.status().ToString(); + auto actual = table_to_test_string(result.ValueOrDie()->table()); + EXPECT_EQ(actual, expected_table) << "Query:\n" << query_text; + + const auto& stats = result.ValueOrDie()->execution_stats(); + const auto it = stats.planned_conditions.find(PlannedPredicateSite::Traverse); + ASSERT_NE(it, stats.planned_conditions.end()); + EXPECT_EQ(std::ranges::count_if(it->second.begin(), it->second.end(), + [](const PlannedPredicateStat& stat) { + return stat.mode == + PlannedPredicateMode::Consume; + }), + 0); + EXPECT_EQ(std::ranges::count_if(it->second.begin(), it->second.end(), + [](const PlannedPredicateStat& stat) { + return stat.mode == + PlannedPredicateMode::PrefilterOnly; + }), + 1); + EXPECT_EQ(stats.deferred_conditions.size(), 1u); +} + +/* +TundraQL query: +MATCH (u:users)-[:works-at RIGHT]->(c:companies) +WHERE u.name = "alex" +SELECT u.name, c.name; + +Expected output table: +u.name | c.name +alex | google +*/ +TEST_F(JoinWhereTest, RightJoinSourceWhereDropsNullSourceRows) { + const std::string query_text = + "MATCH (u:users)-[:works-at RIGHT]->(c:companies)\n" + "WHERE u.name = \"alex\"\n" + "SELECT u.name, c.name;"; + const std::string expected_table = + "u.name | c.name\n" + "alex | google"; + + auto query = + Query::match("u:users") + .traverse("u", "works-at", "c:companies", TraverseType::Right) + .where("u.name", CompareOp::Eq, Value("alex"s)) + .select({"u.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:works-at RIGHT]->(c:companies) +WHERE c.name = "meta" +SELECT u.name, c.name; + +Expected output table: +u.name | c.name +NULL | meta +*/ +TEST_F(JoinWhereTest, RightJoinTargetWhereKeepsUnmatchedTargetRow) { + const std::string query_text = + "MATCH (u:users)-[:works-at RIGHT]->(c:companies)\n" + "WHERE c.name = \"meta\"\n" + "SELECT u.name, c.name;"; + const std::string expected_table = + "u.name | c.name\n" + "NULL | meta"; + + auto query = + Query::match("u:users") + .traverse("u", "works-at", "c:companies", TraverseType::Right) + .where("c.name", CompareOp::Eq, Value("meta"s)) + .select({"u.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:works-at FULL]->(c:companies) +WHERE u.name = "jeff" +SELECT u.name, c.name; + +Expected output table: +u.name | c.name +jeff | NULL +*/ +TEST_F(JoinWhereTest, FullJoinSourceWhereKeepsUnmatchedSourceRow) { + const std::string query_text = + "MATCH (u:users)-[:works-at FULL]->(c:companies)\n" + "WHERE u.name = \"jeff\"\n" + "SELECT u.name, c.name;"; + const std::string expected_table = + "u.name | c.name\n" + "jeff | NULL"; + + auto query = Query::match("u:users") + .traverse("u", "works-at", "c:companies", TraverseType::Full) + .where("u.name", CompareOp::Eq, Value("jeff"s)) + .select({"u.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:works-at FULL]->(c:companies) +WHERE c.name = "meta" +SELECT u.name, c.name; + +Expected output table: +u.name | c.name +NULL | meta +*/ +TEST_F(JoinWhereTest, FullJoinTargetWhereKeepsUnmatchedTargetRow) { + const std::string query_text = + "MATCH (u:users)-[:works-at FULL]->(c:companies)\n" + "WHERE c.name = \"meta\"\n" + "SELECT u.name, c.name;"; + const std::string expected_table = + "u.name | c.name\n" + "NULL | meta"; + + auto query = Query::match("u:users") + .traverse("u", "works-at", "c:companies", TraverseType::Full) + .where("c.name", CompareOp::Eq, Value("meta"s)) + .select({"u.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:friend INNER]->(f:users)-[:works-at INNER]->(c:companies) +WHERE f.name = "bob" AND c.name = "acme" +SELECT u.name, f.name, c.name; + +Expected output table: +u.name | f.name | c.name +alex | bob | acme +*/ +TEST_F(JoinWhereTest, TwoHopInnerJoinMiddleAndTargetWhere) { + const std::string query_text = + "MATCH (u:users)-[:friend INNER]->(f:users)-[:works-at INNER]->" + "(c:companies)\n" + "WHERE f.name = \"bob\" AND c.name = \"acme\"\n" + "SELECT u.name, f.name, c.name;"; + const std::string expected_table = + "u.name | f.name | c.name\n" + "alex | bob | acme"; + + auto query = + Query::match("u:users") + .traverse("u", "friend", "f:users", TraverseType::Inner) + .traverse("f", "works-at", "c:companies", TraverseType::Inner) + .where("f.name", CompareOp::Eq, Value("bob"s)) + .and_where("c.name", CompareOp::Eq, Value("acme"s)) + .select({"u.name", "f.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:friend INNER]->(f:users)-[:works-at LEFT]->(c:companies) +WHERE f.name = "jeff" +SELECT u.name, f.name, c.name; + +Expected output table: +u.name | f.name | c.name +alex | jeff | NULL +*/ +TEST_F(JoinWhereTest, TwoHopLeftJoinMiddleWhereKeepsNullExtendedTargetRow) { + const std::string query_text = + "MATCH (u:users)-[:friend INNER]->(f:users)-[:works-at LEFT]->" + "(c:companies)\n" + "WHERE f.name = \"jeff\"\n" + "SELECT u.name, f.name, c.name;"; + const std::string expected_table = + "u.name | f.name | c.name\n" + "alex | jeff | NULL"; + + auto query = Query::match("u:users") + .traverse("u", "friend", "f:users", TraverseType::Inner) + .traverse("f", "works-at", "c:companies", TraverseType::Left) + .where("f.name", CompareOp::Eq, Value("jeff"s)) + .select({"u.name", "f.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +/* +TundraQL query: +MATCH (u:users)-[:friend INNER]->(f:users)-[:works-at LEFT]->(c:companies) +WHERE u.name = "alex" AND f.name = "bob" AND c.name = "acme" +SELECT u.name, f.name, c.name; + +Expected output table: +u.name | f.name | c.name +alex | bob | acme +*/ +TEST_F(JoinWhereTest, TwoHopLeftJoinRootMiddleAndTargetWhere) { + const std::string query_text = + "MATCH (u:users)-[:friend INNER]->(f:users)-[:works-at LEFT]->" + "(c:companies)\n" + "WHERE u.name = \"alex\" AND f.name = \"bob\" AND c.name = \"acme\"\n" + "SELECT u.name, f.name, c.name;"; + const std::string expected_table = + "u.name | f.name | c.name\n" + "alex | bob | acme"; + + auto query = Query::match("u:users") + .traverse("u", "friend", "f:users", TraverseType::Inner) + .traverse("f", "works-at", "c:companies", TraverseType::Left) + .where("u.name", CompareOp::Eq, Value("alex"s)) + .and_where("f.name", CompareOp::Eq, Value("bob"s)) + .and_where("c.name", CompareOp::Eq, Value("acme"s)) + .select({"u.name", "f.name", "c.name"}) + .inline_where() + .build(); + + expect_query_output(query, query_text, expected_table); +} + +} // namespace tundradb diff --git a/tests/snapshot_test.cpp b/tests/snapshot_test.cpp index 1fd7cd7..74725d4 100644 --- a/tests/snapshot_test.cpp +++ b/tests/snapshot_test.cpp @@ -349,7 +349,7 @@ TEST_F(DatabaseSnapshotTest, SnapshotReloadPreservesEdgeSchemaProperties) { auto new_db = create_test_database(); ASSERT_TRUE(new_db->initialize().ValueOrDie()); - auto q = Query::from("u:User") + auto q = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner, "e") .select({"u.name", "e.since", "e.role"}) .build(); diff --git a/tests/temporal_query_test.cpp b/tests/temporal_query_test.cpp index 07c8d7a..cd6ff23 100644 --- a/tests/temporal_query_test.cpp +++ b/tests/temporal_query_test.cpp @@ -115,7 +115,7 @@ TEST_F(TemporalQueryTest, NodeUpdateAtDifferentTimes) { ASSERT_TRUE(update_result2.ok()) << update_result2.status(); // Query current version (at t2): should see age=27 - auto query_current = Query::from("u:User") + auto query_current = Query::match("u:User") .where("u.name", CompareOp::Eq, Value("Alice")) .build(); auto result_current = db_->query(query_current); @@ -135,7 +135,7 @@ TEST_F(TemporalQueryTest, NodeUpdateAtDifferentTimes) { // ======================================================================== // Query AS OF t0: should see age=25 (original version) - auto query_t0 = Query::from("u:User") + auto query_t0 = Query::match("u:User") .as_of_valid_time(t0_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -150,7 +150,7 @@ TEST_F(TemporalQueryTest, NodeUpdateAtDifferentTimes) { EXPECT_EQ(age_array_t0->Value(0), 25); // Versioning enabled! // Query AS OF t1: should see age=26 (first update) - auto query_t1 = Query::from("u:User") + auto query_t1 = Query::match("u:User") .as_of_valid_time(t1_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -165,7 +165,7 @@ TEST_F(TemporalQueryTest, NodeUpdateAtDifferentTimes) { EXPECT_EQ(age_array_t1->Value(0), 26); // Versioning enabled! // Query AS OF t2: should see age=27 (second update) - auto query_t2 = Query::from("u:User") + auto query_t2 = Query::match("u:User") .as_of_valid_time(t2_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -196,7 +196,7 @@ TEST_F(TemporalQueryTest, MultipleFieldUpdateAtSameTime) { ASSERT_TRUE(update2.ok()); // Query at current time: should see age=31, active=false - auto query = Query::from("u:User") + auto query = Query::match("u:User") .where("u.name", CompareOp::Eq, Value("Bob")) .build(); @@ -237,7 +237,7 @@ TEST_F(TemporalQueryTest, ClockAdvanceAndQuery) { ASSERT_TRUE(update2.ok()); // Query current: should see age=37 - auto query = Query::from("u:User") + auto query = Query::match("u:User") .where("u.name", CompareOp::Eq, Value("Charlie")) .build(); @@ -256,7 +256,7 @@ TEST_F(TemporalQueryTest, ClockAdvanceAndQuery) { // ======================================================================== // Query AS OF creation_time: should see age=35 - auto query_creation = Query::from("u:User") + auto query_creation = Query::match("u:User") .as_of_valid_time(creation_time) .where("u.name", CompareOp::Eq, Value("Charlie")) .build(); @@ -269,7 +269,7 @@ TEST_F(TemporalQueryTest, ClockAdvanceAndQuery) { EXPECT_EQ(age_creation->Value(0), 35); // Query AS OF update1_time: should see age=36 - auto query_update1 = Query::from("u:User") + auto query_update1 = Query::match("u:User") .as_of_valid_time(update1_time) .where("u.name", CompareOp::Eq, Value("Charlie")) .build(); @@ -282,7 +282,7 @@ TEST_F(TemporalQueryTest, ClockAdvanceAndQuery) { EXPECT_EQ(age_update1->Value(0), 36); // Query AS OF update2_time: should see age=37 - auto query_update2 = Query::from("u:User") + auto query_update2 = Query::match("u:User") .as_of_valid_time(update2_time) .where("u.name", CompareOp::Eq, Value("Charlie")) .build(); @@ -317,7 +317,7 @@ TEST_F(TemporalQueryTest, BitemporalQueryWithUpdates) { // ======================================================================== // Query AS OF (valid=t0, tx=t0): should see age=40 - auto query_t0_t0 = Query::from("u:User") + auto query_t0_t0 = Query::match("u:User") .as_of(t0_, t0_) .where("u.name", CompareOp::Eq, Value("Diana")) .build(); @@ -329,7 +329,7 @@ TEST_F(TemporalQueryTest, BitemporalQueryWithUpdates) { EXPECT_EQ(age_t0_t0->Value(0), 40); // Query AS OF (valid=t1, tx=t1): should see age=41 - auto query_t1_t1 = Query::from("u:User") + auto query_t1_t1 = Query::match("u:User") .as_of(t1_, t1_) .where("u.name", CompareOp::Eq, Value("Diana")) .build(); @@ -341,7 +341,7 @@ TEST_F(TemporalQueryTest, BitemporalQueryWithUpdates) { EXPECT_EQ(age_t1_t1->Value(0), 41); // Query AS OF (valid=t2, tx=t2): should see age=42 - auto query_t2_t2 = Query::from("u:User") + auto query_t2_t2 = Query::match("u:User") .as_of(t2_, t2_) .where("u.name", CompareOp::Eq, Value("Diana")) .build(); @@ -354,7 +354,7 @@ TEST_F(TemporalQueryTest, BitemporalQueryWithUpdates) { // Query AS OF (valid=t0, tx=t2): "What did we know at t2 about t0?" // Should see age=40 (the value that was true at t0) - auto query_t0_tx_t2 = Query::from("u:User") + auto query_t0_tx_t2 = Query::match("u:User") .as_of(t0_, t2_) .where("u.name", CompareOp::Eq, Value("Diana")) .build(); @@ -382,7 +382,7 @@ TEST_F(TemporalQueryTest, TemporalQueryBetweenUpdateTimes) { uint64_t t_mid = (t0_ + t1_) / 2; // Query AS OF t_mid (between t0 and t1): should see age=50 (the t0 version) - auto query_mid = Query::from("u:User") + auto query_mid = Query::match("u:User") .as_of_valid_time(t_mid) .where("u.name", CompareOp::Eq, Value("Eve")) .build(); @@ -400,7 +400,7 @@ TEST_F(TemporalQueryTest, CurrentVersionQuery) { int64_t user_id = create_simple_user("Alice", 27); // Query current version (no AS OF clause) - auto query = Query::from("u:User") + auto query = Query::match("u:User") .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -423,7 +423,7 @@ TEST_F(TemporalQueryTest, AsOfValidTimeQuery) { int64_t user_id = create_simple_user("Alice", 25); // Query at t1 - auto query = Query::from("u:User") + auto query = Query::match("u:User") .as_of_valid_time(t1_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -445,7 +445,7 @@ TEST_F(TemporalQueryTest, AsOfTxTimeQuery) { int64_t user_id = create_simple_user("Alice", 26); // Query as of transaction time t1 - auto query = Query::from("u:User") + auto query = Query::match("u:User") .as_of_tx_time(t1_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -467,7 +467,7 @@ TEST_F(TemporalQueryTest, BitemporalQuery) { int64_t user_id = create_simple_user("Alice", 26); // Query both dimensions: valid_time=t1, tx_time=t1 - auto query = Query::from("u:User") + auto query = Query::match("u:User") .as_of(t1_, t1_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -490,7 +490,7 @@ TEST_F(TemporalQueryTest, TemporalQueryWithWhereClause) { create_simple_user("Bob", 30); // Query at t0 where age > 26 (should find only Bob) - auto query = Query::from("u:User") + auto query = Query::match("u:User") .as_of_valid_time(t0_) .where("u.age", CompareOp::Gt, Value(26)) .build(); @@ -517,7 +517,7 @@ TEST_F(TemporalQueryTest, TemporalSnapshotInQueryState) { int64_t user_id = create_simple_user("Alice", 25); // Create query with temporal snapshot - auto query = Query::from("u:User") + auto query = Query::match("u:User") .as_of_valid_time(t1_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -539,7 +539,7 @@ TEST_F(TemporalQueryTest, QueryBeforeFirstVersion) { // Query before t0 (should return current data as versioning not fully enabled // yet) uint64_t before_t0 = t0_ - 1000000000ULL; // 1 second before t0 - auto query = Query::from("u:User") + auto query = Query::match("u:User") .as_of_valid_time(before_t0) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -555,28 +555,28 @@ TEST_F(TemporalQueryTest, QueryBeforeFirstVersion) { TEST_F(TemporalQueryTest, AsOfBuilderMethods) { // Test as_of_valid_time() - auto query1 = Query::from("u:User").as_of_valid_time(t1_).build(); + auto query1 = Query::match("u:User").as_of_valid_time(t1_).build(); ASSERT_TRUE(query1.temporal_snapshot().has_value()); EXPECT_EQ(query1.temporal_snapshot()->valid_time, t1_); EXPECT_EQ(query1.temporal_snapshot()->tx_time, std::numeric_limits::max()); // Test as_of_tx_time() - auto query2 = Query::from("u:User").as_of_tx_time(t2_).build(); + auto query2 = Query::match("u:User").as_of_tx_time(t2_).build(); ASSERT_TRUE(query2.temporal_snapshot().has_value()); EXPECT_EQ(query2.temporal_snapshot()->valid_time, std::numeric_limits::max()); EXPECT_EQ(query2.temporal_snapshot()->tx_time, t2_); // Test as_of() with both dimensions - auto query3 = Query::from("u:User").as_of(t1_, t2_).build(); + auto query3 = Query::match("u:User").as_of(t1_, t2_).build(); ASSERT_TRUE(query3.temporal_snapshot().has_value()); EXPECT_EQ(query3.temporal_snapshot()->valid_time, t1_); EXPECT_EQ(query3.temporal_snapshot()->tx_time, t2_); // Test chaining: as_of_valid_time() then as_of_tx_time() auto query4 = - Query::from("u:User").as_of_valid_time(t1_).as_of_tx_time(t2_).build(); + Query::match("u:User").as_of_valid_time(t1_).as_of_tx_time(t2_).build(); ASSERT_TRUE(query4.temporal_snapshot().has_value()); EXPECT_EQ(query4.temporal_snapshot()->valid_time, t1_); EXPECT_EQ(query4.temporal_snapshot()->tx_time, t2_); @@ -600,7 +600,7 @@ TEST_F(TemporalQueryTest, NullFieldInVersionChain) { ASSERT_TRUE(update2.ok()); // Query at t0: should see age=25 - auto query_t0 = Query::from("u:User") + auto query_t0 = Query::match("u:User") .as_of_valid_time(t0_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -616,7 +616,7 @@ TEST_F(TemporalQueryTest, NullFieldInVersionChain) { EXPECT_EQ(age_array_t0->Value(0), 25); // Query at t1: should see age=30 - auto query_t1 = Query::from("u:User") + auto query_t1 = Query::match("u:User") .as_of_valid_time(t1_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -632,7 +632,7 @@ TEST_F(TemporalQueryTest, NullFieldInVersionChain) { EXPECT_EQ(age_array_t1->Value(0), 30); // Query at t2: should see age=NULL - auto query_t2 = Query::from("u:User") + auto query_t2 = Query::match("u:User") .as_of_valid_time(t2_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -655,7 +655,7 @@ TEST_F(TemporalQueryTest, NodeNotVisibleBeforeCreation) { // Query at t0 (before node creation): should return 0 rows // Because the node's valid_from = t1, it shouldn't be visible at t0 - auto query_before = Query::from("u:User") + auto query_before = Query::match("u:User") .as_of_valid_time(t0_) .build(); // No WHERE clause - get all users at t0 auto result_before = db_->query(query_before); @@ -667,7 +667,7 @@ TEST_F(TemporalQueryTest, NodeNotVisibleBeforeCreation) { EXPECT_EQ(table_before->num_rows(), 0); // Query at t1 (at creation): should return 1 row - auto query_at_creation = Query::from("u:User") + auto query_at_creation = Query::match("u:User") .as_of_valid_time(t1_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -676,7 +676,7 @@ TEST_F(TemporalQueryTest, NodeNotVisibleBeforeCreation) { EXPECT_EQ(result_at.ValueOrDie()->table()->num_rows(), 1); // Query at t2 (after creation): should also return 1 row - auto query_after = Query::from("u:User") + auto query_after = Query::match("u:User") .as_of_valid_time(t2_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -711,7 +711,7 @@ TEST_F(TemporalQueryTest, MultipleNodesIndependentVersions) { // Query at t0: should see only Alice (age=25) // Bob's valid_from = t1 > t0, so Bob should NOT be visible at t0 - auto query_t0 = Query::from("u:User").as_of_valid_time(t0_).build(); + auto query_t0 = Query::match("u:User").as_of_valid_time(t0_).build(); auto result_t0 = db_->query(query_t0); ASSERT_TRUE(result_t0.ok()); auto table_t0 = result_t0.ValueOrDie()->table(); @@ -730,14 +730,14 @@ TEST_F(TemporalQueryTest, MultipleNodesIndependentVersions) { EXPECT_EQ(age_array_t0->Value(0), 25); // Query at t1: should see Alice (age=26) and Bob (age=30) - auto query_t1 = Query::from("u:User").as_of_valid_time(t1_).build(); + auto query_t1 = Query::match("u:User").as_of_valid_time(t1_).build(); auto result_t1 = db_->query(query_t1); ASSERT_TRUE(result_t1.ok()); auto table_t1 = result_t1.ValueOrDie()->table(); EXPECT_EQ(table_t1->num_rows(), 2); // Query at t2: should see Alice (age=26) and Bob (age=31) - auto query_t2 = Query::from("u:User").as_of_valid_time(t2_).build(); + auto query_t2 = Query::match("u:User").as_of_valid_time(t2_).build(); auto result_t2 = db_->query(query_t2); ASSERT_TRUE(result_t2.ok()); auto table_t2 = result_t2.ValueOrDie()->table(); @@ -789,7 +789,7 @@ TEST_F(TemporalQueryTest, VersioningDisabledFallback) { // Temporal query at t0 (should return CURRENT version, not historical) // Because versioning is disabled, no history is kept - auto query_past = Query::from("u:User") + auto query_past = Query::match("u:User") .as_of_valid_time(t0_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -807,7 +807,7 @@ TEST_F(TemporalQueryTest, VersioningDisabledFallback) { EXPECT_EQ(age_array->Value(0), 26); // Current value, not historical // Current query should also return age=26 - auto query_current = Query::from("u:User") + auto query_current = Query::match("u:User") .where("u.name", CompareOp::Eq, Value("Alice")) .build(); auto result_current = db_no_version->query(query_current); @@ -870,7 +870,7 @@ TEST_F(TemporalQueryTest, NoOpUpdateDoesNotCreateNewVersion) { EXPECT_EQ(version_count_after, version_count_before + 1); // Query at t0: should see age=25 - auto query_t0 = Query::from("u:User") + auto query_t0 = Query::match("u:User") .as_of_valid_time(t0_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); @@ -885,7 +885,7 @@ TEST_F(TemporalQueryTest, NoOpUpdateDoesNotCreateNewVersion) { EXPECT_EQ(age_array_t0->Value(0), 25); // Query at t1: should also see age=25 (no change) - auto query_t1 = Query::from("u:User") + auto query_t1 = Query::match("u:User") .as_of_valid_time(t1_) .where("u.name", CompareOp::Eq, Value("Alice")) .build(); diff --git a/tests/update_query_join_test.cpp b/tests/update_query_join_test.cpp index 933c6c9..f10c6c4 100644 --- a/tests/update_query_join_test.cpp +++ b/tests/update_query_join_test.cpp @@ -84,7 +84,7 @@ class UpdateJoinCrossSchemaTest : public ::testing::Test { template T get_field(const std::string& schema, int64_t id, const std::string& field_name) { - auto query = Query::from("_:" + schema).build(); + auto query = Query::match("_:" + schema).build(); auto result = db_->query(query).ValueOrDie(); auto table = result->table(); auto ids = get_column_values(table, "_.id").ValueOrDie(); @@ -112,7 +112,7 @@ TEST_F(UpdateJoinCrossSchemaTest, UpdateBothSidesOfTraversal) { // MATCH (u:User)-[:WORKS_AT]->(c:Company) // WHERE c.name = "Acme" // SET u.employed = true, c.size = 1 - auto q = Query::from("u:User") + auto q = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company") .where("c.name", CompareOp::Eq, Value("Acme"s)) .build(); @@ -144,7 +144,8 @@ TEST_F(UpdateJoinCrossSchemaTest, UpdateBothSidesOfTraversal) { TEST_F(UpdateJoinCrossSchemaTest, UpdateOnlyUserSide) { // Only update User.employed, leave Company untouched - auto q = Query::from("u:User").traverse("u", "WORKS_AT", "c:Company").build(); + auto q = + Query::match("u:User").traverse("u", "WORKS_AT", "c:Company").build(); auto uq = UpdateQuery::match(q).set("u.employed", Value(true)).build(); auto result = db_->update(uq); @@ -161,7 +162,7 @@ TEST_F(UpdateJoinCrossSchemaTest, UpdateOnlyUserSide) { TEST_F(UpdateJoinCrossSchemaTest, UpdateOnlyCompanySide) { // Only update Company.size, leave User untouched - auto q = Query::from("u:User") + auto q = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company") .where("c.name", CompareOp::Eq, Value("Acme"s)) .build(); @@ -180,7 +181,7 @@ TEST_F(UpdateJoinCrossSchemaTest, UpdateOnlyCompanySide) { } TEST_F(UpdateJoinCrossSchemaTest, UpdateWithEdgeAliasTraversal) { - auto q = Query::from("u:User") + auto q = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner, std::optional{"e"}) .where("c.name", CompareOp::Eq, Value("Acme"s)) @@ -197,7 +198,7 @@ TEST_F(UpdateJoinCrossSchemaTest, UpdateWithEdgeAliasTraversal) { } TEST_F(UpdateJoinCrossSchemaTest, FilterByEdgeFieldAndSelectEdgeFields) { - auto query = Query::from("u:User") + auto query = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner, std::optional{"e"}) .where("e.since", CompareOp::Gte, Value(int64_t(2021))) @@ -220,7 +221,7 @@ TEST_F(UpdateJoinCrossSchemaTest, FilterByEdgeFieldAndSelectEdgeFields) { } TEST_F(UpdateJoinCrossSchemaTest, UpdateEdgeFieldByMatchAlias) { - auto q = Query::from("u:User") + auto q = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner, std::optional{"e"}) .where("u.name", CompareOp::Eq, Value("Alice"s)) @@ -232,7 +233,7 @@ TEST_F(UpdateJoinCrossSchemaTest, UpdateEdgeFieldByMatchAlias) { EXPECT_EQ(update_res.ValueOrDie().failed_count, 0); EXPECT_EQ(update_res.ValueOrDie().updated_count, 1); - auto verify = Query::from("u:User") + auto verify = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner, std::optional{"e"}) .where("u.name", CompareOp::Eq, Value("Alice"s)) @@ -245,7 +246,7 @@ TEST_F(UpdateJoinCrossSchemaTest, UpdateEdgeFieldByMatchAlias) { } TEST_F(UpdateJoinCrossSchemaTest, SelectEdgeAliasReturnsOnlyUserDefinedFields) { - auto query = Query::from("u:User") + auto query = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner, std::optional{"e"}) .select({"e"}) @@ -281,7 +282,7 @@ TEST_F(UpdateJoinCrossSchemaTest, SelectEdgeAliasReturnsOnlyUserDefinedFields) { TEST_F(UpdateJoinCrossSchemaTest, TraversalWithNoMatchUpdatesNothing) { // WHERE c.name = "NonExistent" → no rows - auto q = Query::from("u:User") + auto q = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company") .where("c.name", CompareOp::Eq, Value("NonExistent"s)) .build(); @@ -302,7 +303,7 @@ TEST_F(UpdateJoinCrossSchemaTest, TraversalWithNoMatchUpdatesNothing) { TEST_F(UpdateJoinCrossSchemaTest, DuplicateAliasForNodeAndEdgeFails) { // "u" is already used as a node alias (u:User); reusing it as an edge alias // must fail during query preparation. - auto query = Query::from("u:User") + auto query = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner, std::optional{"u"}) .build(); @@ -354,7 +355,7 @@ class UpdateJoinSameSchemaTest : public ::testing::Test { template T get_field(const std::string& schema, int64_t id, const std::string& field_name) { - auto query = Query::from("_:" + schema).build(); + auto query = Query::match("_:" + schema).build(); auto result = db_->query(query).ValueOrDie(); auto table = result->table(); auto ids = get_column_values(table, "_.id").ValueOrDie(); @@ -382,7 +383,7 @@ TEST_F(UpdateJoinSameSchemaTest, UpdateBothSidesOfFriendship) { // MATCH (u:User)-[:FRIEND]->(f:User) // SET u.has_friend = true, f.has_friend = true - auto q = Query::from("u:User").traverse("u", "FRIEND", "f:User").build(); + auto q = Query::match("u:User").traverse("u", "FRIEND", "f:User").build(); auto uq = UpdateQuery::match(q) .set("u.has_friend", Value(true)) .set("f.has_friend", Value(true)) @@ -407,7 +408,7 @@ TEST_F(UpdateJoinSameSchemaTest, UpdateBothSidesOfFriendship) { TEST_F(UpdateJoinSameSchemaTest, UpdateOnlySourceSide) { // Only update the source alias "u" - auto q = Query::from("u:User").traverse("u", "FRIEND", "f:User").build(); + auto q = Query::match("u:User").traverse("u", "FRIEND", "f:User").build(); auto uq = UpdateQuery::match(q).set("u.has_friend", Value(true)).build(); auto result = db_->update(uq); @@ -424,7 +425,7 @@ TEST_F(UpdateJoinSameSchemaTest, UpdateOnlySourceSide) { TEST_F(UpdateJoinSameSchemaTest, UpdateOnlyTargetSide) { // Only update the target alias "f" - auto q = Query::from("u:User").traverse("u", "FRIEND", "f:User").build(); + auto q = Query::match("u:User").traverse("u", "FRIEND", "f:User").build(); auto uq = UpdateQuery::match(q).set("f.has_friend", Value(true)).build(); auto result = db_->update(uq); @@ -444,7 +445,7 @@ TEST_F(UpdateJoinSameSchemaTest, UpdateOnlyTargetSide) { TEST_F(UpdateJoinSameSchemaTest, UpdateWithWhereOnTarget) { // Only update friends named "Bob" - auto q = Query::from("u:User") + auto q = Query::match("u:User") .traverse("u", "FRIEND", "f:User") .where("f.name", CompareOp::Eq, Value("Bob"s)) .build(); diff --git a/tests/update_query_test.cpp b/tests/update_query_test.cpp index 9c63525..04d667a 100644 --- a/tests/update_query_test.cpp +++ b/tests/update_query_test.cpp @@ -87,7 +87,7 @@ class UpdateQueryTest : public ::testing::Test { T get_field(const std::string& schema, int64_t id, const std::string& field_name) { const std::string alias = "_"; - auto query = Query::from(alias + ":" + schema).build(); + auto query = Query::match(alias + ":" + schema).build(); auto result = db_->query(query).ValueOrDie(); auto table = result->table(); auto ids = get_column_values(table, alias + ".id").ValueOrDie(); @@ -142,12 +142,12 @@ TEST_F(UpdateQueryTest, BuilderDefaultUpdateTypeIsSET) { // ========================================================================= TEST_F(UpdateQueryTest, MatchRequiresAtLeastOneSet) { - auto q = Query::from("u:User").build(); + auto q = Query::match("u:User").build(); EXPECT_THROW((UpdateQuery::match(q).build()), std::runtime_error); } TEST_F(UpdateQueryTest, MatchStoresQuery) { - auto q = Query::from("u:User") + auto q = Query::match("u:User") .where("u.city", CompareOp::Eq, Value("NYC"s)) .build(); auto uq = UpdateQuery::match(q).set("u.age", Value(31)).build(); @@ -156,7 +156,8 @@ TEST_F(UpdateQueryTest, MatchStoresQuery) { } TEST_F(UpdateQueryTest, MatchTargetAliasesFromSetFields) { - auto q = Query::from("u:User").traverse("u", "WORKS_AT", "c:Company").build(); + auto q = + Query::match("u:User").traverse("u", "WORKS_AT", "c:Company").build(); auto uq = UpdateQuery::match(q) .set("u.salary", Value(int32_t(0))) .set("c.size", Value(int32_t(9))) @@ -247,7 +248,7 @@ TEST_F(UpdateQueryTest, UpdateByIdInvalidSchema) { TEST_F(UpdateQueryTest, UpdateByMatchSimpleWhere) { // All NYC users: Alice(0), Bob(1), Eve(4) - auto q = Query::from("u:User") + auto q = Query::match("u:User") .where("u.city", CompareOp::Eq, Value("NYC"s)) .build(); auto uq = @@ -270,7 +271,7 @@ TEST_F(UpdateQueryTest, UpdateByMatchSimpleWhere) { } TEST_F(UpdateQueryTest, UpdateByMatchSingleResult) { - auto q = Query::from("u:User") + auto q = Query::match("u:User") .where("u.name", CompareOp::Eq, Value("Alice"s)) .build(); auto uq = UpdateQuery::match(q).set("u.age", Value(int32_t(26))).build(); @@ -283,7 +284,7 @@ TEST_F(UpdateQueryTest, UpdateByMatchSingleResult) { } TEST_F(UpdateQueryTest, UpdateByMatchNoResults) { - auto q = Query::from("u:User") + auto q = Query::match("u:User") .where("u.name", CompareOp::Eq, Value("Nobody"s)) .build(); auto uq = UpdateQuery::match(q).set("u.age", Value(int32_t(0))).build(); @@ -295,7 +296,7 @@ TEST_F(UpdateQueryTest, UpdateByMatchNoResults) { TEST_F(UpdateQueryTest, UpdateByMatchCompoundAnd) { // age > 30 AND city = "NYC" → Bob(35,NYC), Eve(55,NYC) - auto q = Query::from("u:User") + auto q = Query::match("u:User") .where("u.age", CompareOp::Gt, Value(int32_t(30))) .and_where("u.city", CompareOp::Eq, Value("NYC"s)) .build(); @@ -311,7 +312,7 @@ TEST_F(UpdateQueryTest, UpdateByMatchCompoundAnd) { } TEST_F(UpdateQueryTest, UpdateByMatchMultipleSetFields) { - auto q = Query::from("u:User") + auto q = Query::match("u:User") .where("u.name", CompareOp::Eq, Value("Alice"s)) .build(); auto uq = UpdateQuery::match(q) @@ -333,7 +334,7 @@ TEST_F(UpdateQueryTest, UpdateByMatchMultipleSetFields) { TEST_F(UpdateQueryTest, UpdateByMatchWithTraversal) { // Update users who work at TechCorp: Alice(0), Bob(1) - auto q = Query::from("u:User") + auto q = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company") .where("c.name", CompareOp::Eq, Value("TechCorp"s)) .build(); @@ -357,7 +358,7 @@ TEST_F(UpdateQueryTest, UpdateByMatchWithTraversal) { TEST_F(UpdateQueryTest, UpdateMultiSchemaViaTraversal) { // UPDATE users who work at TechCorp AND update TechCorp itself - auto q = Query::from("u:User") + auto q = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company") .where("c.name", CompareOp::Eq, Value("TechCorp"s)) .build(); @@ -384,7 +385,7 @@ TEST_F(UpdateQueryTest, UpdateMultiSchemaViaTraversal) { // ========================================================================= TEST_F(UpdateQueryTest, UpdateByMatchBadAliasInSet) { - auto q = Query::from("u:User").build(); + auto q = Query::match("u:User").build(); auto uq = UpdateQuery::match(q) .set("x.salary", Value(int32_t(0))) // "x" not in MATCH .build(); @@ -394,7 +395,7 @@ TEST_F(UpdateQueryTest, UpdateByMatchBadAliasInSet) { } TEST_F(UpdateQueryTest, UpdateByMatchUnqualifiedFieldFails) { - auto q = Query::from("u:User").build(); + auto q = Query::match("u:User").build(); auto uq = UpdateQuery::match(q) .set("salary", Value(int32_t(0))) // missing alias .build(); @@ -469,7 +470,7 @@ TEST_F(UpdateQueryTest, UpdateByMatchSupportsMapKeySet) { db_->create_node("MapUser", {{"name", Value{"Nina"}}}).ValueOrDie(); db_->create_node("MapUser", {{"name", Value{"Omar"}}}).ValueOrDie(); - auto q = Query::from("m:MapUser") + auto q = Query::match("m:MapUser") .where("m.name", CompareOp::Eq, Value("Nina"s)) .build(); auto uq = @@ -506,7 +507,7 @@ TEST_F(UpdateQueryTest, db_->get_schema_registry()->create("MapUserDepth", map_schema).ValueOrDie(); db_->create_node("MapUserDepth", {{"name", Value{"Nina"}}}).ValueOrDie(); - auto q = Query::from("m:MapUserDepth") + auto q = Query::match("m:MapUserDepth") .where("m.name", CompareOp::Eq, Value("Nina"s)) .build(); auto uq = UpdateQuery::match(q) diff --git a/tests/where_expression_test.cpp b/tests/where_expression_test.cpp index 3b8c57d..9196388 100644 --- a/tests/where_expression_test.cpp +++ b/tests/where_expression_test.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,24 @@ using namespace std::string_literals; using namespace tundradb; namespace tundradb { + +namespace { + +size_t count_planned_predicates(const QueryExecutionStats& stats, + PlannedPredicateSite site, + PlannedPredicateMode mode) { + const auto it = stats.planned_conditions.find(site); + if (it == stats.planned_conditions.end()) { + return 0; + } + + return static_cast(std::ranges::count_if( + it->second, + [mode](const PlannedPredicateStat& stat) { return stat.mode == mode; })); +} + +} // namespace + class WhereExpressionTest : public ::testing::Test { protected: void SetUp() override { @@ -135,7 +154,8 @@ class WhereExpressionTest : public ::testing::Test { // Test simple WHERE expressions TEST_F(WhereExpressionTest, SimpleWhereCondition) { // Test basic WHERE clause - Query query = Query::from("u:User").where("u.age", CompareOp::Gt, 40).build(); + Query query = + Query::match("u:User").where("u.age", CompareOp::Gt, 40).build(); auto result = db_->query(query); ASSERT_OK(result); @@ -153,7 +173,7 @@ TEST_F(WhereExpressionTest, SimpleWhereCondition) { // Test compound WHERE with AND - fluent API TEST_F(WhereExpressionTest, CompoundWhereAndFluent) { // Test: age > 30 AND city = "NYC" - Query query = Query::from("u:User") + Query query = Query::match("u:User") .where("u.age", CompareOp::Gt, 30) .and_where("u.city", CompareOp::Eq, "NYC") .build(); @@ -178,7 +198,7 @@ TEST_F(WhereExpressionTest, CompoundWhereAndFluent) { TEST_F(WhereExpressionTest, CompoundWhereOrFluent) { Logger::get_instance().set_level(LogLevel::DEBUG); // Test: city = "SF" OR salary > 150000 - Query query = Query::from("u:User") + Query query = Query::match("u:User") .where("u.city", CompareOp::Eq, "SF") .or_where("u.salary", CompareOp::Gt, 150000) .build(); @@ -220,7 +240,7 @@ TEST_F(WhereExpressionTest, ComplexExpressionWithPrecedence) { // age > 30 AND (city = "NYC" OR salary > 150000) auto final_expr = LogicalExpr::and_expr(age_condition, or_expr); - Query query = Query::from("u:User").where_logical_expr(final_expr).build(); + Query query = Query::match("u:User").where_logical_expr(final_expr).build(); auto result = db_->query(query); ASSERT_OK(result); @@ -245,7 +265,7 @@ TEST_F(WhereExpressionTest, ComplexExpressionWithPrecedence) { // Test inline WHERE with simple condition TEST_F(WhereExpressionTest, InlineWhereSimple) { // Test inline optimization with simple WHERE - Query query = Query::from("u:User") + Query query = Query::match("u:User") .traverse("u", "FRIEND", "f:User") .where("f.age", CompareOp::Gt, 40) .inline_where() @@ -268,7 +288,7 @@ TEST_F(WhereExpressionTest, InlineWhereSimple) { // Test inline WHERE with compound condition TEST_F(WhereExpressionTest, InlineWhereCompound) { // Test inline optimization with compound WHERE: f.age > 25 AND f.city = "NYC" - Query query = Query::from("u:User") + Query query = Query::match("u:User") .traverse("u", "FRIEND", "f:User") .where("f.age", CompareOp::Gt, 25) .and_where("f.city", CompareOp::Eq, "NYC") @@ -302,7 +322,7 @@ TEST_F(WhereExpressionTest, MultipleDifferentPrecedence) { // Left-to-right: (age > 40 AND city = "LA") OR salary > 100000 // This will match: Bob(salary), Charlie(salary), Eve(salary), Henry(salary), // Jack(both) = 5 users - Query query_left = Query::from("u:User") + Query query_left = Query::match("u:User") .where("u.age", CompareOp::Gt, 40) .and_where("u.city", CompareOp::Eq, "LA") .or_where("u.salary", CompareOp::Gt, 100000) @@ -325,7 +345,7 @@ TEST_F(WhereExpressionTest, MultipleDifferentPrecedence) { auto final_expr = LogicalExpr::and_expr(age_cond, or_part); Query query_explicit = - Query::from("u:User").where_logical_expr(final_expr).build(); + Query::match("u:User").where_logical_expr(final_expr).build(); auto result_explicit = db_->query(query_explicit); ASSERT_OK(result_explicit); @@ -368,7 +388,7 @@ TEST_F(WhereExpressionTest, ExpressionToString) { // Test error handling TEST_F(WhereExpressionTest, ErrorHandling) { // Test invalid field name - Query query = Query::from("u:User") + Query query = Query::match("u:User") .where("u.nonexistent", CompareOp::Eq, "value") .build(); @@ -444,7 +464,7 @@ TEST_F(WhereExpressionTest, PerformanceComparison) { // Test simple WHERE performance auto start = std::chrono::high_resolution_clock::now(); - Query query = Query::from("u:User") + Query query = Query::match("u:User") .where("u.age", CompareOp::Gt, 40) .and_where("u.city", CompareOp::Eq, "NYC") .build(); @@ -505,7 +525,7 @@ TEST_F(WhereExpressionTest, OrWithMultipleVariablesNotInlined) { auto final_expr = LogicalExpr::and_expr(age_condition, or_expr); // Create query that should match our test data - Query query = Query::from("a:User") + Query query = Query::match("a:User") .traverse("a", "WORKS_AT", "c:Company") .select({"a.age", "a.city", "c.size"}) // Explicitly select the fields we need @@ -540,7 +560,7 @@ TEST_F(WhereExpressionTest, TraversalWhereCombinations) { // Test Case 1: Single variable where clause (should be inlined) { - Query query = Query::from("u:User") + Query query = Query::match("u:User") .where("u.age", CompareOp::Gt, 35) .traverse("u", "WORKS_AT", "c:Company") @@ -561,7 +581,7 @@ TEST_F(WhereExpressionTest, TraversalWhereCombinations) { } TEST_F(WhereExpressionTest, TraversalWhereCombinations2) { - Query query = Query::from("u:User") + Query query = Query::match("u:User") .traverse("u", "WORKS_AT", "c:Company") .where("u.age", CompareOp::Gte, 35) .and_where("c.size", CompareOp::Gt, 1000) @@ -582,7 +602,7 @@ TEST_F(WhereExpressionTest, TraversalWhereCombinations2) { TEST_F(WhereExpressionTest, TraversalWhereCombinations3) { Query query = - Query::from("u:User") + Query::match("u:User") .where("u.age", CompareOp::Gte, 35) // Should be inlined .traverse("u", "WORKS_AT", "c:Company") .where("c.size", CompareOp::Gt, 1000) // Should be inlined @@ -600,8 +620,15 @@ TEST_F(WhereExpressionTest, TraversalWhereCombinations3) { ASSERT_EQ(table->num_rows(), 0); const auto& stats = result.ValueOrDie()->execution_stats(); - EXPECT_EQ(stats.num_where_clauses_inlined, 2); - EXPECT_EQ(stats.num_where_clauses_post_processed, 1); + EXPECT_EQ(stats.num_where_clauses_inlined, 4); + EXPECT_EQ(stats.num_where_clauses_post_processed, 0); + EXPECT_EQ(count_planned_predicates(stats, PlannedPredicateSite::Root, + PlannedPredicateMode::Consume), + 2u); + EXPECT_EQ(count_planned_predicates(stats, PlannedPredicateSite::Traverse, + PlannedPredicateMode::Consume), + 2u); + EXPECT_TRUE(stats.deferred_conditions.empty()); } TEST_F(WhereExpressionTest, QueryMaterializesMapColumn) { @@ -620,7 +647,7 @@ TEST_F(WhereExpressionTest, QueryMaterializesMapColumn) { std::vector{"score"}}}) .ok()); - Query query = Query::from("m:MapUser").build(); + Query query = Query::match("m:MapUser").build(); auto result = db_->query(query); ASSERT_OK(result); @@ -662,7 +689,7 @@ TEST_F(WhereExpressionTest, QueryFiltersByMapProperty) { ASSERT_OK(ben->update_fields( {FieldUpdate{props, Value{int32_t(7)}, UpdateType::SET, score_key}})); - Query query = Query::from("m:MapUserFilter") + Query query = Query::match("m:MapUserFilter") .where("m.props.score", CompareOp::Eq, Value(int32_t(42))) .build(); auto result = db_->query(query); @@ -676,4 +703,4 @@ TEST_F(WhereExpressionTest, QueryFiltersByMapProperty) { EXPECT_EQ(names[0], "Anna"); } -} // namespace tundradb \ No newline at end of file +} // namespace tundradb diff --git a/tests/where_planner_test.cpp b/tests/where_planner_test.cpp new file mode 100644 index 0000000..aea5ae2 --- /dev/null +++ b/tests/where_planner_test.cpp @@ -0,0 +1,194 @@ +#include "query/where_planner.hpp" + +#include + +#include +#include +#include + +#include "core/edge_store.hpp" +#include "query/execution.hpp" +#include "schema/schema.hpp" + +using namespace std::string_literals; +using namespace tundradb; + +namespace tundradb { + +class WherePlannerTest : public ::testing::Test { + protected: + void SetUp() override { + registry_ = std::make_shared(); + + auto user_res = registry_->create( + "User", arrow::schema({arrow::field("x", arrow::int32())})); + ASSERT_TRUE(user_res.ok()) << user_res.status().ToString(); + + auto company_res = registry_->create( + "Company", arrow::schema({arrow::field("z", arrow::int32()), + arrow::field("country", arrow::utf8())})); + ASSERT_TRUE(company_res.ok()) << company_res.status().ToString(); + + auto region_res = registry_->create( + "Region", arrow::schema({arrow::field("kind", arrow::utf8())})); + ASSERT_TRUE(region_res.ok()) << region_res.status().ToString(); + + edge_store_ = std::make_shared(0); + + auto works_at_res = edge_store_->register_edge_schema( + "WORKS_AT", {std::make_shared("y", ValueType::INT32)}); + ASSERT_TRUE(works_at_res.ok()) << works_at_res.status().ToString(); + + auto located_in_res = edge_store_->register_edge_schema( + "LOCATED_IN", {std::make_shared("weight", ValueType::INT32)}); + ASSERT_TRUE(located_in_res.ok()) << located_in_res.status().ToString(); + } + + void prepare_state(const Query& query, QueryState& state) const { + state.edge_store = edge_store_; + + auto status = prepare_query(query, state); + EXPECT_TRUE(status.ok()) << status.ToString(); + } + + std::shared_ptr registry_; + std::shared_ptr edge_store_; +}; + +TEST_F(WherePlannerTest, SplitsAndAcrossRootTargetAndEdge) { + auto query = Query::match("u:User") + .traverse("u", "WORKS_AT", "c:Company", TraverseType::Inner, + std::optional{"e"}) + .where("u.x", CompareOp::Eq, Value(int32_t(1))) + .and_where("e.y", CompareOp::Eq, Value(int32_t(2))) + .and_where("c.z", CompareOp::Eq, Value(int32_t(3))) + .build(); + + QueryState state(registry_); + prepare_state(query, state); + auto plan_res = build_where_plan(query, state); + ASSERT_TRUE(plan_res.ok()) << plan_res.status().ToString(); + const auto& plan = plan_res.ValueOrDie(); + + ASSERT_EQ(plan.root_filters.size(), 1u); + EXPECT_EQ(plan.root_filters[0].source_clause_index, 1u); + EXPECT_EQ(plan.root_filters[0].expr->extract_first_variable(), "u"); + EXPECT_EQ(plan.root_filters[0].mode, PlannedPredicateMode::Consume); + + ASSERT_EQ(plan.traverse_filters.size(), 1u); + ASSERT_EQ(plan.traverse_filters[0].target_filters.size(), 1u); + ASSERT_EQ(plan.traverse_filters[0].edge_filters.size(), 1u); + EXPECT_EQ(plan.traverse_filters[0].target_filters[0].source_clause_index, 1u); + EXPECT_EQ(plan.traverse_filters[0].edge_filters[0].source_clause_index, 1u); + EXPECT_EQ(plan.traverse_filters[0].target_filters[0].mode, + PlannedPredicateMode::Consume); + EXPECT_EQ(plan.traverse_filters[0].edge_filters[0].mode, + PlannedPredicateMode::Consume); + EXPECT_EQ( + plan.traverse_filters[0].target_filters[0].expr->extract_first_variable(), + "c"); + EXPECT_EQ( + plan.traverse_filters[0].edge_filters[0].expr->extract_first_variable(), + "e"); + + ASSERT_EQ(plan.residual_by_clause.size(), 2u); + EXPECT_EQ(plan.residual_by_clause[1], nullptr); +} + +TEST_F(WherePlannerTest, PullsLaterAliasFiltersBackToEarliestTraverseSlot) { + auto query = Query::match("u:User") + .traverse("u", "WORKS_AT", "c:Company") + .where("c.z", CompareOp::Eq, Value(int32_t(3))) + .traverse("c", "LOCATED_IN", "r:Region") + .where("c.country", CompareOp::Eq, Value("US"s)) + .and_where("r.kind", CompareOp::Eq, Value("hq"s)) + .build(); + + QueryState state(registry_); + prepare_state(query, state); + auto plan_res = build_where_plan(query, state); + ASSERT_TRUE(plan_res.ok()) << plan_res.status().ToString(); + const auto& plan = plan_res.ValueOrDie(); + + ASSERT_EQ(plan.traverse_filters.size(), 2u); + ASSERT_EQ(plan.traverse_filters[0].target_filters.size(), 2u); + EXPECT_EQ(plan.traverse_filters[0].target_filters[0].source_clause_index, 1u); + EXPECT_EQ(plan.traverse_filters[0].target_filters[1].source_clause_index, 3u); + EXPECT_EQ(plan.traverse_filters[0].target_filters[0].mode, + PlannedPredicateMode::Consume); + EXPECT_EQ(plan.traverse_filters[0].target_filters[1].mode, + PlannedPredicateMode::Consume); + EXPECT_EQ( + plan.traverse_filters[0].target_filters[0].expr->extract_first_variable(), + "c"); + EXPECT_EQ( + plan.traverse_filters[0].target_filters[1].expr->extract_first_variable(), + "c"); + + ASSERT_EQ(plan.traverse_filters[1].target_filters.size(), 1u); + EXPECT_EQ(plan.traverse_filters[1].target_filters[0].source_clause_index, 3u); + EXPECT_EQ(plan.traverse_filters[1].target_filters[0].mode, + PlannedPredicateMode::Consume); + EXPECT_EQ( + plan.traverse_filters[1].target_filters[0].expr->extract_first_variable(), + "r"); + + ASSERT_EQ(plan.residual_by_clause.size(), 4u); + EXPECT_EQ(plan.residual_by_clause[1], nullptr); + EXPECT_EQ(plan.residual_by_clause[3], nullptr); +} + +TEST_F(WherePlannerTest, KeepsMixedAliasOrAsResidual) { + auto expr = LogicalExpr::or_expr( + std::make_shared("u.x", CompareOp::Eq, Value(int32_t(1))), + std::make_shared("c.z", CompareOp::Eq, + Value(int32_t(3)))); + + auto query = Query::match("u:User") + .traverse("u", "WORKS_AT", "c:Company") + .where_logical_expr(expr) + .build(); + + QueryState state(registry_); + prepare_state(query, state); + auto plan_res = build_where_plan(query, state); + ASSERT_TRUE(plan_res.ok()) << plan_res.status().ToString(); + const auto& plan = plan_res.ValueOrDie(); + + EXPECT_TRUE(plan.root_filters.empty()); + ASSERT_EQ(plan.traverse_filters.size(), 1u); + EXPECT_TRUE(plan.traverse_filters[0].target_filters.empty()); + EXPECT_TRUE(plan.traverse_filters[0].edge_filters.empty()); + + ASSERT_EQ(plan.residual_by_clause.size(), 2u); + ASSERT_NE(plan.residual_by_clause[1], nullptr); + EXPECT_EQ(plan.residual_by_clause[1]->get_all_variables().size(), 2u); +} + +TEST_F(WherePlannerTest, PrefiltersNullableLeftTargetAndKeepsResidual) { + auto query = Query::match("u:User") + .traverse("u", "WORKS_AT", "c:Company", TraverseType::Left) + .where("c.z", CompareOp::Eq, Value(int32_t(3))) + .traverse("c", "LOCATED_IN", "r:Region") + .build(); + + QueryState state(registry_); + prepare_state(query, state); + auto plan_res = build_where_plan(query, state); + ASSERT_TRUE(plan_res.ok()) << plan_res.status().ToString(); + const auto& plan = plan_res.ValueOrDie(); + + ASSERT_EQ(plan.traverse_filters.size(), 2u); + ASSERT_EQ(plan.traverse_filters[0].target_filters.size(), 1u); + EXPECT_EQ(plan.traverse_filters[0].target_filters[0].mode, + PlannedPredicateMode::PrefilterOnly); + EXPECT_EQ( + plan.traverse_filters[0].target_filters[0].expr->extract_first_variable(), + "c"); + + ASSERT_EQ(plan.residual_by_clause.size(), 3u); + ASSERT_NE(plan.residual_by_clause[1], nullptr); + EXPECT_EQ(plan.residual_by_clause[1]->extract_first_variable(), "c"); +} + +} // namespace tundradb diff --git a/tests/where_pushdown_join_test.cpp b/tests/where_pushdown_join_test.cpp index 9fba8ce..e191452 100644 --- a/tests/where_pushdown_join_test.cpp +++ b/tests/where_pushdown_join_test.cpp @@ -84,7 +84,7 @@ TEST_F(WherePushdownJoinTest, WhereInJoin) { auto duration = std::chrono::duration_cast( end_time - start_time); create_companies({"google", "ibm", "piedpiper"}); - Query query = Query::from("u:User").build(); + Query query = Query::match("u:User").build(); auto result = db_->query(query); std::cout << result.ValueOrDie()->table()->num_rows() @@ -97,7 +97,7 @@ TEST_F(WherePushdownJoinTest, WhereInJoin) { db_->connect(i, "FRIEND", i + half).ValueOrDie(); } - query = Query::from("u:User").traverse("u", "FRIEND", "f:User").build(); + query = Query::match("u:User").traverse("u", "FRIEND", "f:User").build(); result = db_->query(query); @@ -119,7 +119,7 @@ TEST_F(WherePushdownJoinTest, WhereInJoin) { << std::endl; start_time = std::chrono::high_resolution_clock::now(); - result = db_->query(Query::from("u:User") + result = db_->query(Query::match("u:User") .traverse("u", "FRIEND", "f:User") .where("f.age", CompareOp::Gt, 50) .build()); @@ -134,7 +134,7 @@ TEST_F(WherePushdownJoinTest, WhereInJoin) { std::cout << "unoptimized size=" << unoptimized_size << std::endl; start_time = std::chrono::high_resolution_clock::now(); - result = db_->query(Query::from("u:User") + result = db_->query(Query::match("u:User") .traverse("u", "FRIEND", "f:User") .where("f.age", CompareOp::Gt, 50) .inline_where()