use anyhow::Result;
use surrealdb_types::Value;
use test_log::test;
use crate::catalog::providers::CatalogProvider;
use crate::dbs::{NewPlannerStrategy, Session};
use crate::kvs::{Datastore, TransactionType};
const SETUP: &str = "
DEFINE ANALYZER simple TOKENIZERS blank;
DEFINE TABLE t SCHEMALESS;
DEFINE INDEX ft ON t FIELDS content FULLTEXT ANALYZER simple BM25;
CREATE t:1 SET flag = true, content = 'alpha bravo';
CREATE t:2 SET flag = false, content = 'charlie delta';
CREATE t:3 SET flag = true, content = 'echo foxtrot';
";
const NO_INDEX: &str = "no suitable index supporting";
async fn select_ids(
strategy: NewPlannerStrategy,
query: &str,
) -> Result<Result<Vec<String>, String>> {
let ds = Datastore::new("memory").await?;
{
let tx = ds.transaction(TransactionType::Write).await?;
tx.ensure_ns_db(None, "test", "test").await?;
tx.commit().await?;
}
let root = Session::owner().with_ns("test").with_db("test");
for response in ds.execute(SETUP, &root, None).await? {
response.result?;
}
let reader = Session::owner().with_ns("test").with_db("test").new_planner_strategy(strategy);
let mut responses = ds.execute(query, &reader, None).await?;
let value = match responses.remove(0).result {
Ok(value) => value,
Err(err) => return Ok(Err(err.to_string())),
};
let Value::Array(rows) = value else {
return Ok(Err(format!("expected an array of rows, got {value:?}")));
};
let mut ids: Vec<String> =
rows.into_iter().map(|row| surrealdb_types::ToSql::to_sql(&row)).collect();
ids.sort();
Ok(Ok(ids))
}
async fn both_engines(
query: &str,
) -> Result<(Result<Vec<String>, String>, Result<Vec<String>, String>)> {
let legacy = select_ids(NewPlannerStrategy::ComputeOnly, query).await?;
let streaming = select_ids(NewPlannerStrategy::AllReadOnlyStatements, query).await?;
Ok((legacy, streaming))
}
fn assert_both_refuse(legacy: Result<Vec<String>, String>, streaming: Result<Vec<String>, String>) {
let legacy = legacy.expect_err("the legacy engine must refuse a MATCHES that names no index");
let streaming =
streaming.expect_err("the streaming engine must refuse a MATCHES that names no index");
assert!(legacy.contains(NO_INDEX), "legacy refusal does not name the missing index: {legacy}");
assert_eq!(legacy, streaming, "the two engines word the same refusal differently");
}
#[test(tokio::test(flavor = "multi_thread"))]
async fn both_engines_refuse_a_matches_inside_an_if_else_branch() -> Result<()> {
let (legacy, streaming) = both_engines(
"SELECT VALUE id FROM t WHERE (IF flag = true { content @@ 'bravo' } ELSE { true })",
)
.await?;
assert_both_refuse(legacy, streaming);
Ok(())
}
#[test(tokio::test(flavor = "multi_thread"))]
async fn both_engines_refuse_a_matches_inside_a_function_argument() -> Result<()> {
let (legacy, streaming) =
both_engines("SELECT VALUE id FROM t WHERE array::any([content @@ 'bravo'])").await?;
assert_both_refuse(legacy, streaming);
Ok(())
}
#[test(tokio::test(flavor = "multi_thread"))]
async fn both_engines_answer_a_top_level_matches() -> Result<()> {
let (legacy, streaming) =
both_engines("SELECT VALUE id FROM t WHERE content @@ 'bravo'").await?;
assert_eq!(legacy, streaming, "the two engines disagree on a top-level MATCHES");
assert_eq!(legacy.expect("a top-level MATCHES is answerable"), vec!["t:1".to_string()]);
Ok(())
}
#[test(tokio::test(flavor = "multi_thread"))]
async fn both_engines_answer_a_matches_inside_a_subquery() -> Result<()> {
let (legacy, streaming) = both_engines(
"SELECT VALUE id FROM t WHERE id IN (SELECT VALUE id FROM t WHERE content @@ 'bravo')",
)
.await?;
assert_eq!(legacy, streaming, "the two engines disagree on a MATCHES inside a subquery");
assert_eq!(legacy.expect("a subquery's own MATCHES is answerable"), vec!["t:1".to_string()]);
Ok(())
}
#[test(tokio::test(flavor = "multi_thread"))]
async fn both_engines_answer_a_registered_matches_duplicated_inside_a_branch() -> Result<()> {
let (legacy, streaming) = both_engines(
"SELECT VALUE id FROM t WHERE content @@ 'bravo' \
AND (IF flag = true { content @@ 'bravo' } ELSE { true })",
)
.await?;
assert_eq!(legacy, streaming, "the two engines disagree on a duplicated registered MATCHES");
assert_eq!(
legacy.expect("a registered MATCHES resolves wherever it is written"),
vec!["t:1".to_string()],
);
Ok(())
}
#[test(tokio::test(flavor = "multi_thread"))]
async fn both_engines_answer_a_registered_matches_inside_a_projection_block() -> Result<()> {
let (legacy, streaming) =
both_engines("SELECT VALUE { RETURN content @@ 'bravo'; } FROM t WHERE content @@ 'bravo'")
.await?;
assert_eq!(
legacy, streaming,
"the two engines disagree on a registered MATCHES in a projection block"
);
assert_eq!(
legacy.expect("a registered MATCHES resolves in projection position"),
vec!["true".to_string()],
);
Ok(())
}
#[test(tokio::test(flavor = "multi_thread"))]
async fn both_engines_refuse_an_unregistered_matches_inside_a_projection_block() -> Result<()> {
let (legacy, streaming) =
both_engines("SELECT VALUE { RETURN content @@ 'zulu'; } FROM t").await?;
assert_both_refuse(legacy, streaming);
Ok(())
}
#[test(tokio::test(flavor = "multi_thread"))]
async fn engines_differ_on_a_negated_matches() -> Result<()> {
let (legacy, streaming) =
both_engines("SELECT VALUE id FROM t WHERE !(content @@ 'bravo')").await?;
let legacy = legacy.expect_err("the legacy engine does not register the operand of a NOT");
assert!(legacy.contains(NO_INDEX), "legacy refusal does not name the missing index: {legacy}");
assert_eq!(
streaming.expect("the streaming engine registers the operand of a NOT"),
vec!["t:2".to_string(), "t:3".to_string()],
);
Ok(())
}