diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index db587676de7..8326ce06f2c 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -1314,10 +1314,15 @@ fn default_row_count_fn(db: Identity) -> RowCountFn { #[cfg(any(test, feature = "test"))] pub mod tests_utils { use super::*; + use crate::sql::ast::{SchemaViewer, TableSchemaView}; use core::ops::Deref; use durability::EmptyHistory; + use expect_test::Expect; + use spacetimedb_lib::identity::AuthCtx; use spacetimedb_lib::{bsatn::to_vec, ser::Serialize}; use spacetimedb_paths::FromPathUnchecked; + use spacetimedb_physical_plan::plan::tests_utils::*; + use spacetimedb_physical_plan::printer::ExplainOptions; use tempfile::TempDir; /// A [`RelationalDB`] in a temporary directory. @@ -1605,6 +1610,20 @@ pub mod tests_utils { Self(log) } } + + pub fn expect_query(tx: &T, sql: &str, expect: Expect) { + let auth = AuthCtx::for_testing(); + let schema = SchemaViewer::new(tx, &auth); + + check_query(&schema, ExplainOptions::default(), sql, expect) + } + + pub fn expect_sub(tx: &T, sql: &str, expect: Expect) { + let auth = AuthCtx::for_testing(); + let schema = SchemaViewer::new(tx, &auth); + + check_sub(&schema, ExplainOptions::default(), sql, expect) + } } #[cfg(test)] diff --git a/crates/core/src/sql/compiler.rs b/crates/core/src/sql/compiler.rs index 90441eed9a6..5ec1e3805fe 100644 --- a/crates/core/src/sql/compiler.rs +++ b/crates/core/src/sql/compiler.rs @@ -231,16 +231,18 @@ fn compile_statement(db: &RelationalDB, statement: SqlAst) -> Result( db: &RelationalDB, tx: &T, @@ -277,69 +275,6 @@ mod tests { super::compile_sql(db, &AuthCtx::for_testing(), tx, sql) } - #[test] - fn compile_eq() -> ResultTest<()> { - let db = TestDB::durable()?; - - // Create table [test] without any indexes - let schema = &[("a", AlgebraicType::U64)]; - let indexes = &[]; - db.create_table_for_test("test", schema, indexes)?; - - let tx = db.begin_tx(Workload::ForTests); - // Compile query - let sql = "select * from test where a = 1"; - let CrudExpr::Query(QueryExpr { source: _, query }) = compile_sql(&db, &tx, sql)?.remove(0) else { - panic!("Expected QueryExpr"); - }; - assert_eq!(1, query.len()); - assert_select(&query[0]); - Ok(()) - } - - #[test] - fn compile_not_eq() -> ResultTest<()> { - let db = TestDB::durable()?; - - // Create table [test] with cols [a, b] and index on [b]. - db.create_table_for_test( - "test", - &[("a", AlgebraicType::U64), ("b", AlgebraicType::U64)], - &[1.into(), 0.into()], - )?; - - let tx = db.begin_tx(Workload::ForTests); - // Should work with any qualified field. - let sql = "select * from test where a = 1 and b <> 3"; - let CrudExpr::Query(QueryExpr { source: _, query }) = compile_sql(&db, &tx, sql)?.remove(0) else { - panic!("Expected QueryExpr"); - }; - assert_eq!(2, query.len()); - assert_one_eq_index_scan(&query[0], 0, 1u64.into()); - assert_select(&query[1]); - Ok(()) - } - - #[test] - fn compile_index_eq_basic() -> ResultTest<()> { - let db = TestDB::durable()?; - - // Create table [test] with index on [a] - let schema = &[("a", AlgebraicType::U64)]; - let indexes = &[0.into()]; - db.create_table_for_test("test", schema, indexes)?; - - let tx = db.begin_tx(Workload::ForTests); - //Compile query - let sql = "select * from test where a = 1"; - let CrudExpr::Query(QueryExpr { source: _, query }) = compile_sql(&db, &tx, sql)?.remove(0) else { - panic!("Expected QueryExpr"); - }; - assert_eq!(1, query.len()); - assert_one_eq_index_scan(&query[0], 0, 1u64.into()); - Ok(()) - } - #[test] fn compile_eq_identity_address() -> ResultTest<()> { let db = TestDB::durable()?; @@ -383,6 +318,7 @@ mod tests { let rows = run_for_testing(&db, sql)?; + //TODO(sql): Move the check of the 'plan' to use 'EXPLAIN' let CrudExpr::Query(QueryExpr { source: _, query: mut ops, @@ -457,24 +393,81 @@ mod tests { } #[test] - fn compile_eq_and_eq() -> ResultTest<()> { + fn compile_eq() -> ResultTest<()> { let db = TestDB::durable()?; - // Create table [test] with index on [b] - let schema = &[("a", AlgebraicType::U64), ("b", AlgebraicType::U64)]; - let indexes = &[1.into()]; + // Create table [test] without any indexes + let schema = &[("a", AlgebraicType::U64)]; + let indexes = &[]; db.create_table_for_test("test", schema, indexes)?; let tx = db.begin_tx(Workload::ForTests); - // Note, order does not matter. - // The sargable predicate occurs last, but we can still generate an index scan. - let sql = "select * from test where a = 1 and b = 2"; - let CrudExpr::Query(QueryExpr { source: _, query }) = compile_sql(&db, &tx, sql)?.remove(0) else { - panic!("Expected QueryExpr"); - }; - assert_eq!(2, query.len()); - assert_one_eq_index_scan(&query[0], 1, 2u64.into()); - assert_select(&query[1]); + + expect_sub( + &tx, + "select * from test where a = 1", + expect![ + r#" + Seq Scan on test + Filter: (test.a = U64(1)) + Output: test.a"# + ], + ); + + Ok(()) + } + + #[test] + fn compile_not_eq() -> ResultTest<()> { + let db = TestDB::durable()?; + + // Create table [test] with cols [a, b] and index on [b]. + db.create_table_for_test( + "test", + &[("a", AlgebraicType::U64), ("b", AlgebraicType::U64)], + &[1.into(), 0.into()], + )?; + + let tx = db.begin_tx(Workload::ForTests); + // Should work with any qualified field. + + expect_sub( + &tx, + "select * from test where a = 1 and b <> 3", + expect![ + r#" +Index Scan using Index test_a_idx_btree on test + Index Cond: (test.a = U64(1)) + Filter: (test.b <> U64(3)) + Output: test.a, test.b"# + ], + ); + + Ok(()) + } + + #[test] + fn compile_index_eq_basic() -> ResultTest<()> { + let db = TestDB::durable()?; + + // Create table [test] with index on [a] + let schema = &[("a", AlgebraicType::U64)]; + let indexes = &[0.into()]; + db.create_table_for_test("test", schema, indexes)?; + + let tx = db.begin_tx(Workload::ForTests); + + expect_query( + &tx, + "select * from test where a = 1", + expect![ + r#" +Index Scan using Index test_a_idx_btree on test + Index Cond: (test.a = U64(1)) + Output: test.a"# + ], + ); + Ok(()) } @@ -490,13 +483,18 @@ mod tests { let tx = db.begin_tx(Workload::ForTests); // Note, order does not matter. // The sargable predicate occurs first adn we can generate an index scan. - let sql = "select * from test where b = 2 and a = 1"; - let CrudExpr::Query(QueryExpr { source: _, query }) = compile_sql(&db, &tx, sql)?.remove(0) else { - panic!("Expected QueryExpr"); - }; - assert_eq!(2, query.len()); - assert_one_eq_index_scan(&query[0], 1, 2u64.into()); - assert_select(&query[1]); + expect_query( + &tx, + "select * from test where b = 2 and a = 1", + expect![ + r#" +Index Scan using Index test_b_idx_btree on test + Index Cond: (test.b = U64(2)) + Filter: (test.a = U64(1)) + Output: test.a, test.b"# + ], + ); + Ok(()) } @@ -520,6 +518,20 @@ mod tests { }; assert_eq!(1, query.len()); assert_one_eq_index_scan(&query[0], col_list![0, 1], product![1u64, 2u64].into()); + + //TODO(sql): Need support for multi-column indexes + expect_query( + &tx, + sql, + expect![ + r#" + Index Scan using Index test_a_b_idx_btree: (a, b) on test + Index Cond: (test.a = U64(1)) + Filter: (test.b = U64(2)) //wrong + Output: test.a, test.b, test.c, test.d"# + ], + ); + Ok(()) } @@ -533,14 +545,18 @@ mod tests { db.create_table_for_test("test", schema, indexes)?; let tx = db.begin_tx(Workload::ForTests); - // Compile query - let sql = "select * from test where a = 1 or b = 2"; - let CrudExpr::Query(QueryExpr { source: _, query }) = compile_sql(&db, &tx, sql)?.remove(0) else { - panic!("Expected QueryExpr"); - }; - assert_eq!(1, query.len()); - // Assert no index scan because OR is not sargable. - assert_select(&query[0]); + + expect_query( + &tx, + "select * from test where a = 1 or b = 2", + expect![ + r#" +Seq Scan on test + Filter: (test.a = U64(1) OR test.b = U64(2)) + Output: test.a, test.b"# + ], + ); + Ok(()) } @@ -556,11 +572,18 @@ mod tests { let tx = db.begin_tx(Workload::ForTests); // Compile query let sql = "select * from test where b > 2"; - let CrudExpr::Query(QueryExpr { source: _, query }) = compile_sql(&db, &tx, sql)?.remove(0) else { - panic!("Expected QueryExpr"); - }; - assert_eq!(1, query.len()); - assert_index_scan(&query[0], 1, Bound::Excluded(AlgebraicValue::U64(2)), Bound::Unbounded); + + //TODO(sql): Need support for index scans for ranges + expect_query( + &tx, + sql, + expect![ + r#" + Index Scan on test + Filter: (test.b > U64(2)) + Output: test.a, test.b"# + ], + ); Ok(()) } @@ -588,6 +611,18 @@ mod tests { Bound::Excluded(AlgebraicValue::U64(5)), ); + //TODO(sql): Need support for index scans for ranges + expect_query( + &tx, + sql, + expect![ + r#" + Index Scan on test + Filter: (test.b > U64(2) AND test.b < U64(5)) + Output: test.a, test.b"# + ], + ); + Ok(()) } @@ -603,13 +638,18 @@ mod tests { let tx = db.begin_tx(Workload::ForTests); // Note, order matters - the equality condition occurs first which // means an index scan will be generated rather than the range condition. - let sql = "select * from test where a = 3 and b > 2 and b < 5"; - let CrudExpr::Query(QueryExpr { source: _, query }) = compile_sql(&db, &tx, sql)?.remove(0) else { - panic!("Expected QueryExpr"); - }; - assert_eq!(2, query.len()); - assert_one_eq_index_scan(&query[0], 0, 3u64.into()); - assert_select(&query[1]); + expect_query( + &tx, + "select * from test where a = 3 and b > 2 and b < 5", + expect![ + r#" +Index Scan using Index test_a_idx_btree on test + Index Cond: (test.a = U64(3)) + Filter: (test.b < U64(5) AND test.b > U64(2)) + Output: test.a, test.b"# + ], + ); + Ok(()) } @@ -620,50 +660,30 @@ mod tests { // Create table [lhs] with index on [a] let schema = &[("a", AlgebraicType::U64), ("b", AlgebraicType::U64)]; let indexes = &[0.into()]; - let lhs_id = db.create_table_for_test("lhs", schema, indexes)?; + db.create_table_for_test("lhs", schema, indexes)?; // Create table [rhs] with no indexes let schema = &[("b", AlgebraicType::U64), ("c", AlgebraicType::U64)]; let indexes = &[]; - let rhs_id = db.create_table_for_test("rhs", schema, indexes)?; + db.create_table_for_test("rhs", schema, indexes)?; let tx = db.begin_tx(Workload::ForTests); // Should push sargable equality condition below join - let sql = "select lhs.* from lhs join rhs on lhs.b = rhs.b where lhs.a = 3"; - let exp = compile_sql(&db, &tx, sql)?.remove(0); - - let CrudExpr::Query(QueryExpr { - source: source_lhs, - query, - .. - }) = exp - else { - panic!("unexpected expression: {:#?}", exp); - }; - - assert_eq!(source_lhs.table_id().unwrap(), lhs_id); - assert_eq!(query.len(), 3); - - // First operation in the pipeline should be an index scan - let table_id = assert_one_eq_index_scan(&query[0], 0, 3u64.into()); - - assert_eq!(table_id, lhs_id); - - // Followed by a join with the rhs table - let Query::JoinInner(JoinExpr { - ref rhs, - col_lhs, - col_rhs, - inner: Some(ref inner_header), - }) = query[1] - else { - panic!("unexpected operator {:#?}", query[1]); - }; + expect_sub( + &tx, + "select lhs.* from lhs join rhs on lhs.b = rhs.b where lhs.a = 3", + expect![ + r#" +Hash Join: Lhs + -> Index Scan using Index lhs_a_idx_btree on lhs + Index Cond: (lhs.a = U64(3)) + -> Seq Scan on rhs + Inner Unique: false + Join Cond: (lhs.b = rhs.b) + Output: lhs.a, lhs.b"# + ], + ); - assert_eq!(rhs.source.table_id().unwrap(), rhs_id); - assert_eq!(col_lhs, 1.into()); - assert_eq!(col_rhs, 0.into()); - assert_eq!(&**inner_header, &source_lhs.head().extend(rhs.source.head())); Ok(()) } @@ -673,54 +693,29 @@ mod tests { // Create table [lhs] with no indexes let schema = &[("a", AlgebraicType::U64), ("b", AlgebraicType::U64)]; - let lhs_id = db.create_table_for_test("lhs", schema, &[])?; + db.create_table_for_test("lhs", schema, &[])?; // Create table [rhs] with no indexes let schema = &[("b", AlgebraicType::U64), ("c", AlgebraicType::U64)]; - let rhs_id = db.create_table_for_test("rhs", schema, &[])?; + db.create_table_for_test("rhs", schema, &[])?; let tx = db.begin_tx(Workload::ForTests); // Should push equality condition below join - let sql = "select lhs.* from lhs join rhs on lhs.b = rhs.b where lhs.a = 3"; - let exp = compile_sql(&db, &tx, sql)?.remove(0); - - let CrudExpr::Query(QueryExpr { - source: source_lhs, - query, - .. - }) = exp - else { - panic!("unexpected expression: {:#?}", exp); - }; - assert_eq!(source_lhs.table_id().unwrap(), lhs_id); - assert_eq!(query.len(), 3); - - // The first operation in the pipeline should be a selection with `col#0 = 3` - let Query::Select(ColumnOp::ColCmpVal { - cmp: OpCmp::Eq, - lhs: ColId(0), - rhs: AlgebraicValue::U64(3), - }) = query[0] - else { - panic!("unexpected operator {:#?}", query[0]); - }; - - // The join should follow the selection - let Query::JoinInner(JoinExpr { - ref rhs, - col_lhs, - col_rhs, - inner: Some(ref inner_header), - }) = query[1] - else { - panic!("unexpected operator {:#?}", query[1]); - }; + expect_query( + &tx, + "select lhs.* from lhs join rhs on lhs.b = rhs.b where lhs.a = 3", + expect![ + r#" +Hash Join: Lhs + -> Seq Scan on lhs + Filter: (lhs.a = U64(3)) + -> Seq Scan on rhs + Inner Unique: false + Join Cond: (lhs.b = rhs.b) + Output: lhs.a, lhs.b"# + ], + ); - assert_eq!(rhs.source.table_id().unwrap(), rhs_id); - assert_eq!(col_lhs, 1.into()); - assert_eq!(col_rhs, 0.into()); - assert_eq!(&**inner_header, &source_lhs.head().extend(rhs.source.head())); - assert!(rhs.query.is_empty()); Ok(()) } @@ -730,53 +725,29 @@ mod tests { // Create table [lhs] with no indexes let schema = &[("a", AlgebraicType::U64), ("b", AlgebraicType::U64)]; - let lhs_id = db.create_table_for_test("lhs", schema, &[])?; + db.create_table_for_test("lhs", schema, &[])?; // Create table [rhs] with no indexes let schema = &[("b", AlgebraicType::U64), ("c", AlgebraicType::U64)]; - let rhs_id = db.create_table_for_test("rhs", schema, &[])?; + db.create_table_for_test("rhs", schema, &[])?; let tx = db.begin_tx(Workload::ForTests); - // Should push equality condition below join - let sql = "select lhs.* from lhs join rhs on lhs.b = rhs.b where rhs.c = 3"; - let exp = compile_sql(&db, &tx, sql)?.remove(0); - let CrudExpr::Query(QueryExpr { - source: source_lhs, - query, - .. - }) = exp - else { - panic!("unexpected expression: {:#?}", exp); - }; - - assert_eq!(source_lhs.table_id().unwrap(), lhs_id); - assert_eq!(query.len(), 1); - - // First and only operation in the pipeline should be a join - let Query::JoinInner(JoinExpr { - ref rhs, - col_lhs, - col_rhs, - inner: None, - }) = query[0] - else { - panic!("unexpected operator {:#?}", query[0]); - }; - - assert_eq!(rhs.source.table_id().unwrap(), rhs_id); - assert_eq!(col_lhs, 1.into()); - assert_eq!(col_rhs, 0.into()); + expect_query( + &tx, + "select lhs.* from lhs join rhs on lhs.b = rhs.b where rhs.c = 3", + expect![ + r#" +Hash Join: Rhs + -> Seq Scan on rhs + Filter: (rhs.c = U64(3)) + -> Seq Scan on lhs + Inner Unique: false + Join Cond: (rhs.b = lhs.b) + Output: lhs.a, lhs.b"# + ], + ); - // The selection should be pushed onto the rhs of the join - let Query::Select(ColumnOp::ColCmpVal { - cmp: OpCmp::Eq, - lhs: ColId(1), - rhs: AlgebraicValue::U64(3), - }) = rhs.query[0] - else { - panic!("unexpected operator {:#?}", rhs.query[0]); - }; Ok(()) } @@ -787,63 +758,32 @@ mod tests { // Create table [lhs] with index on [a] let schema = &[("a", AlgebraicType::U64), ("b", AlgebraicType::U64)]; let indexes = &[0.into()]; - let lhs_id = db.create_table_for_test("lhs", schema, indexes)?; + db.create_table_for_test("lhs", schema, indexes)?; // Create table [rhs] with index on [c] let schema = &[("b", AlgebraicType::U64), ("c", AlgebraicType::U64)]; let indexes = &[1.into()]; - let rhs_id = db.create_table_for_test("rhs", schema, indexes)?; + db.create_table_for_test("rhs", schema, indexes)?; let tx = db.begin_tx(Workload::ForTests); // Should push the sargable equality condition into the join's left arg. // Should push the sargable range condition into the join's right arg. - let sql = "select lhs.* from lhs join rhs on lhs.b = rhs.b where lhs.a = 3 and rhs.c < 4"; - let exp = compile_sql(&db, &tx, sql)?.remove(0); - - let CrudExpr::Query(QueryExpr { - source: source_lhs, - query, - .. - }) = exp - else { - panic!("unexpected result from compilation: {:?}", exp); - }; - - assert_eq!(source_lhs.table_id().unwrap(), lhs_id); - assert_eq!(query.len(), 3); - - // First operation in the pipeline should be an index scan - let table_id = assert_one_eq_index_scan(&query[0], 0, 3u64.into()); - - assert_eq!(table_id, lhs_id); - - // Followed by a join - let Query::JoinInner(JoinExpr { - ref rhs, - col_lhs, - col_rhs, - inner: Some(ref inner_header), - }) = query[1] - else { - panic!("unexpected operator {:#?}", query[1]); - }; - - assert_eq!(rhs.source.table_id().unwrap(), rhs_id); - assert_eq!(col_lhs, 1.into()); - assert_eq!(col_rhs, 0.into()); - assert_eq!(&**inner_header, &source_lhs.head().extend(rhs.source.head())); - - assert_eq!(1, rhs.query.len()); - - // The right side of the join should be an index scan - let table_id = assert_index_scan( - &rhs.query[0], - 1, - Bound::Unbounded, - Bound::Excluded(AlgebraicValue::U64(4)), + expect_sub( + &tx, + "select lhs.* from lhs join rhs on lhs.b = rhs.b where lhs.a = 3 and rhs.c < 4", + expect![ + r#" +Hash Join: Lhs + -> Index Scan using Index lhs_a_idx_btree on lhs + Index Cond: (lhs.a = U64(3)) + -> Seq Scan on rhs + Filter: (rhs.c < U64(4)) + Inner Unique: false + Join Cond: (lhs.b = rhs.b) + Output: lhs.a, lhs.b"# + ], ); - assert_eq!(table_id, rhs_id); Ok(()) } @@ -854,7 +794,7 @@ mod tests { // Create table [lhs] with index on [b] let schema = &[("a", AlgebraicType::U64), ("b", AlgebraicType::U64)]; let indexes = &[1.into()]; - let lhs_id = db.create_table_for_test("lhs", schema, indexes)?; + db.create_table_for_test("lhs", schema, indexes)?; // Create table [rhs] with index on [b, c] let schema = &[ @@ -863,69 +803,25 @@ mod tests { ("d", AlgebraicType::U64), ]; let indexes = &[0.into(), 1.into()]; - let rhs_id = db.create_table_for_test("rhs", schema, indexes)?; + db.create_table_for_test("rhs", schema, indexes)?; let tx = db.begin_tx(Workload::ForTests); // Should generate an index join since there is an index on `lhs.b`. // Should push the sargable range condition into the index join's probe side. - let sql = "select lhs.* from lhs join rhs on lhs.b = rhs.b where rhs.c > 2 and rhs.c < 4 and rhs.d = 3"; - let exp = compile_sql(&db, &tx, sql)?.remove(0); - - let CrudExpr::Query(QueryExpr { - source: SourceExpr::DbTable(DbTable { table_id, .. }), - query, - .. - }) = exp - else { - panic!("unexpected result from compilation: {:?}", exp); - }; - - assert_eq!(table_id, lhs_id); - assert_eq!(query.len(), 1); - - let Query::IndexJoin(IndexJoin { - probe_side: - QueryExpr { - source: SourceExpr::DbTable(DbTable { table_id, .. }), - query: rhs, - }, - probe_col, - index_side: SourceExpr::DbTable(DbTable { - table_id: index_table, .. - }), - index_col, - .. - }) = &query[0] - else { - panic!("unexpected operator {:#?}", query[0]); - }; - - assert_eq!(*table_id, rhs_id); - assert_eq!(*index_table, lhs_id); - assert_eq!(index_col, &1.into()); - assert_eq!(*probe_col, 0.into()); - - assert_eq!(2, rhs.len()); - - // The probe side of the join should be an index scan - let table_id = assert_index_scan( - &rhs[0], - 1, - Bound::Excluded(AlgebraicValue::U64(2)), - Bound::Excluded(AlgebraicValue::U64(4)), + expect_sub( + &tx, + "select lhs.* from lhs join rhs on lhs.b = rhs.b where rhs.c > 2 and rhs.c < 4 and rhs.d = 3", + expect![ + r#" +Index Join: Rhs on lhs + -> Seq Scan on rhs + Filter: (rhs.c > U64(2) AND rhs.c < U64(4) AND rhs.d = U64(3)) + Inner Unique: false + Join Cond: (rhs.b = lhs.b) + Output: lhs.a, lhs.b"# + ], ); - assert_eq!(table_id, rhs_id); - - // Followed by a selection - let Query::Select(ColumnOp::ColCmpVal { - cmp: OpCmp::Eq, - lhs: ColId(2), - rhs: AlgebraicValue::U64(3), - }) = rhs[1] - else { - panic!("unexpected operator {:#?}", rhs[0]); - }; Ok(()) } @@ -1003,6 +899,24 @@ mod tests { else { panic!("unexpected operator {:#?}", rhs[0]); }; + + //TODO(sql): Need support for multi-column indexes + expect_sub( + &tx, + sql, + expect![ + r#" + Hash Join: Lhs + -> Seq Scan on lhs + -> Index Scan using Index rhs_b_c_idx_btree: (b, c) on rhs + -> Index Cond: (rhs.b = U64(4)) + -> Filter: (rhs.c = U64(2) AND rhs.d = U64(3)) + Inner Unique: false + Hash Cond: (lhs.b = rhs.b) + Output: lhs.a, lhs.b"# + ], + ); + Ok(()) } @@ -1011,12 +925,22 @@ mod tests { let db = TestDB::durable()?; db.create_table_for_test("A", &[("x", AlgebraicType::U64)], &[])?; db.create_table_for_test("B", &[("y", AlgebraicType::U64)], &[])?; - assert!(compile_sql( - &db, - &db.begin_tx(Workload::ForTests), - "select B.* from B join A on B.y = A.x" - ) - .is_ok()); + + let tx = db.begin_tx(Workload::ForTests); + expect_sub( + &tx, + "select B.* from B join A on B.y = A.x", + expect![ + r#" +Hash Join: Lhs + -> Seq Scan on B + -> Seq Scan on A + Inner Unique: false + Join Cond: (B.y = A.x) + Output: B.y"# + ], + ); + Ok(()) } diff --git a/crates/core/src/subscription/subscription.rs b/crates/core/src/subscription/subscription.rs index 45ecede8db1..d3fffd3c220 100644 --- a/crates/core/src/subscription/subscription.rs +++ b/crates/core/src/subscription/subscription.rs @@ -653,9 +653,10 @@ pub(crate) fn legacy_get_all( #[cfg(test)] mod tests { use super::*; - use crate::db::relational_db::tests_utils::TestDB; + use crate::db::relational_db::tests_utils::{expect_sub, TestDB}; use crate::execution_context::Workload; use crate::sql::compiler::compile_sql; + use expect_test::expect; use spacetimedb_lib::relation::DbTable; use spacetimedb_lib::{error::ResultTest, identity::AuthCtx}; use spacetimedb_sats::{product, AlgebraicType}; @@ -699,6 +700,21 @@ mod tests { panic!("expected an index join, but got {:#?}", join); }; + //TODO(sql): Remove manual checks to just `EXPLAIN` the query. + expect_sub( + &tx, + sql, + expect![ + r#" +Index Join: Rhs on lhs + -> Seq Scan on rhs + Filter: (rhs.c > U64(2) AND rhs.c < U64(4) AND rhs.d = U64(3)) + Inner Unique: false + Join Cond: (rhs.b = lhs.b) + Output: lhs.a, lhs.b"# + ], + ); + // Create an insert for an incremental update. let delta = vec![product![0u64, 0u64]]; @@ -779,6 +795,22 @@ mod tests { panic!("expected an index join, but got {:#?}", join); }; + //TODO(sql): Remove manual checks to just `EXPLAIN` the query. + // Why this generate same plan than the previous test? 'compile_incremental_index_join_index_side' + expect_sub( + &tx, + sql, + expect![ + r#" +Index Join: Rhs on lhs + -> Seq Scan on rhs + Filter: (rhs.c > U64(2) AND rhs.c < U64(4) AND rhs.d = U64(3)) + Inner Unique: false + Join Cond: (rhs.b = lhs.b) + Output: lhs.a, lhs.b"# + ], + ); + // Create an insert for an incremental update. let delta = vec![product![0u64, 0u64, 0u64]]; @@ -868,6 +900,22 @@ mod tests { src_join ); + //TODO(sql): Remove manual checks to just `EXPLAIN` the query. + // Why this generate same plan than the previous test? 'compile_incremental_index_join_index_side' + expect_sub( + &tx, + sql, + expect![ + r#" +Index Join: Rhs on lhs + -> Seq Scan on rhs + Filter: (rhs.c > U64(2) AND rhs.c < U64(4) AND rhs.d = U64(3)) + Inner Unique: false + Join Cond: (rhs.b = lhs.b) + Output: lhs.a, lhs.b"# + ], + ); + let incr = IncrementalJoin::new(&expr).expect("Failed to construct IncrementalJoin"); let virtual_plan = &incr.virtual_plan;