use crate::expr::TopLevelExpr;
fn read_only(sql: &str) -> bool {
let mut ast = crate::syn::parse(sql).expect("fixture should parse");
assert_eq!(ast.expressions.len(), 1, "expected exactly one statement in {sql:?}");
assert_agree(&ast.expressions.remove(0), sql)
}
fn assert_agree(surface: &crate::sql::TopLevelExpr, context: &str) -> bool {
let converted: TopLevelExpr = surface.clone().into();
let answer = surface.read_only();
assert_eq!(
answer,
converted.read_only(),
"`sql::TopLevelExpr::read_only` and `expr::TopLevelExpr::read_only` disagree on {context}"
);
answer
}
fn assert_none_claim_read_only(cases: &[(&str, &str)]) {
let mut exercised = 0;
for (label, sql) in cases {
if crate::syn::parse(sql).is_err() {
println!("skipped (does not parse): {label}: {sql}");
continue;
}
exercised += 1;
assert!(!read_only(sql), "{label}: `{sql}` claimed to be read-only");
}
assert!(exercised > 0, "every fixture was skipped, so this test asserted nothing");
}
#[test]
fn a_mutation_in_any_select_clause_requires_a_write_transaction() {
let cases = [
("projection", "SELECT *, (CREATE log) AS w FROM person"),
("what", "SELECT * FROM (CREATE log)"),
("cond", "SELECT * FROM person WHERE (CREATE log).n > 0"),
("omit", "SELECT * OMIT (CREATE log).n FROM person"),
("limit", "SELECT * FROM person LIMIT (CREATE log).n"),
("start", "SELECT * FROM person START (CREATE log).n"),
("version", "SELECT * FROM person VERSION (CREATE log).n"),
("timeout", "SELECT * FROM person TIMEOUT (CREATE log).n"),
("fetch", "SELECT * FROM person FETCH tags[WHERE (CREATE log)]"),
("split", "SELECT * FROM person SPLIT ON tags[WHERE (CREATE log)]"),
("group", "SELECT tags FROM person GROUP BY tags[WHERE (CREATE log)]"),
("order", "SELECT * FROM person ORDER BY tags[WHERE (CREATE log)]"),
];
assert_none_claim_read_only(&cases);
}
#[test]
fn a_mutation_in_any_idiom_part_requires_a_write_transaction() {
let cases = [
("where filter", "SELECT tags[WHERE (CREATE log)] FROM person"),
("value index", "SELECT tags[(CREATE log).n] FROM person"),
("method argument", "SELECT tags.at((CREATE log).n) FROM person"),
("destructure alias", "SELECT person.{ name, tag: tags[WHERE (CREATE log)] } FROM person"),
("graph lookup cond", "SELECT ->likes[WHERE (CREATE log)]->thing FROM person"),
];
assert_none_claim_read_only(&cases);
}
#[test]
fn a_mutation_in_an_info_subject_or_version_requires_a_write_transaction() {
let cases = [
("root version", "INFO FOR ROOT VERSION (CREATE log:1).id"),
("ns version", "INFO FOR NS VERSION (CREATE log:1).id"),
("db version", "INFO FOR DB VERSION (CREATE log:1).id"),
("table version", "INFO FOR TABLE person VERSION (CREATE log:1).id"),
("table name", "INFO FOR TABLE (CREATE log:1).id"),
("user name", "INFO FOR USER (CREATE log:1).id"),
("index name", "INFO FOR INDEX (CREATE log:1).id ON person"),
("index table", "INFO FOR INDEX idx ON (CREATE log:1).id"),
];
assert_none_claim_read_only(&cases);
}
#[test]
fn a_mutation_in_a_lookup_subject_requires_a_write_transaction() {
let cases = [
("lookup range upper bound", "SELECT * FROM b:1->computed_edge:[6]..=[(CREATE log:1).id]"),
("lookup range lower bound", "SELECT * FROM b:1->computed_edge:[(CREATE log:1).id]..=[6]"),
(
"lookup subject in projection",
"SELECT * FROM person:1->likes:[(CREATE log:2).id]..->post",
),
];
assert_none_claim_read_only(&cases);
}
#[test]
fn a_mutation_in_a_return_fetch_requires_a_write_transaction() {
assert_none_claim_read_only(&[(
"return fetch",
"RETURN person FETCH tags[WHERE (CREATE log)]",
)]);
}
#[test]
fn read_only_statements_keep_their_read_transaction() {
let cases = [
"SELECT * FROM person",
"SELECT *, count() AS n FROM person GROUP ALL",
"SELECT * FROM person WHERE age > 30 ORDER BY name LIMIT 10 START 5",
"SELECT * FROM person FETCH friends",
"SELECT * FROM person SPLIT ON tags",
"SELECT name FROM person GROUP BY name",
"SELECT * FROM person TIMEOUT 5s",
"SELECT ->likes->thing FROM person",
"SELECT tags[WHERE value > 1] FROM person",
"RETURN person FETCH friends",
"RETURN 1 + 2",
"RETURN $s.len()",
"RETURN 'a'.len()",
"SELECT tags.map(|$t| $t + 1) FROM person",
];
for sql in cases {
assert!(read_only(sql), "`{sql}` should not need a write transaction");
}
}
#[test]
fn mutations_require_a_write_transaction() {
for sql in ["CREATE person", "UPDATE person SET a = 1", "DELETE person", "DEFINE TABLE person"]
{
assert!(!read_only(sql), "`{sql}` claimed to be read-only");
}
}
#[test]
fn a_mutation_in_a_composite_literal_requires_a_write_transaction() {
let cases = [
("array literal", "RETURN [(CREATE log)]"),
("set literal", "RETURN <set> [(CREATE log)]"),
("object literal", "RETURN { w: (CREATE log) }"),
("record id key", "RETURN r_thing:[(CREATE log).n]"),
("record id range", "SELECT * FROM r_thing:[(CREATE log).n].."),
];
assert_none_claim_read_only(&cases);
}
#[test]
fn closures_that_may_write_require_a_write_transaction() {
let cases = [
("call on a param", "RETURN $fn(1)"),
("call argument", "RETURN (|$n| $n)((CREATE log).n)"),
("writing literal body, called", "RETURN (|| { CREATE log })()"),
("writing literal body, as a value", "RETURN [|| { CREATE log }]"),
(
"writing literal body, as a builtin argument",
"RETURN array::map([1], || { CREATE log })",
),
("closure field invoked as a method", "RETURN $o.w()"),
("closure field invoked as a postfix method", "RETURN ($o).w()"),
("closure field invoked mid-idiom", "SELECT handlers.w().n FROM person"),
];
assert_none_claim_read_only(&cases);
}
#[test]
fn a_pure_literal_closure_call_keeps_its_read_transaction() {
for sql in ["RETURN (|$n: number| $n + 1)(1)", "SELECT * FROM (|| 1)()"] {
assert!(read_only(sql), "`{sql}` should not need a write transaction");
}
}