// Copyright 2016-2018 Mozilla // // Licensed under the Apache License, Version 2.0 (the "License"); you may not use // this file except in compliance with the License. You may obtain a copy of the // License at http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. #[macro_use] extern crate mentat; // TODO: when we switch to `failure`, make this more humane. //use query_algebrizer_traits; // For errors; use std::str::FromStr; use chrono::FixedOffset; use core_traits::{Entid, KnownEntid, ValueType, ValueTypeSet}; use edn::OrderedFloat; use mentat_core::{DateTime, HasSchema, Utc, Uuid}; use query_projector_traits::aggregates::SimpleAggregationOp; use mentat::{ new_connection, Binding, IntoResult, Keyword, PlainSymbol, QueryInputs, QueryResults, Queryable, RelResult, Store, TxReport, TypedValue, Variable, }; use mentat::query::q_uncached; use mentat::conn::Conn; use public_traits::errors::MentatError; use assert_approx_eq::assert_approx_eq; #[test] fn test_rel() { let mut c = new_connection("").expect("Couldn't open conn."); let db = mentat_db::db::ensure_current_version(&mut c).expect("Couldn't open DB."); // Rel. let start = std::time::Instant::now(); let results = q_uncached( &c, &db.schema, "[:find ?x ?ident :where [?x :db/ident ?ident]]", None, ) .expect("Query failed") .results; let end = std::time::Instant::now(); // This will need to change each time we add a default ident. assert_eq!(40, results.len()); // Every row is a pair of a Ref and a Keyword. if let QueryResults::Rel(rel) = results { for r in rel.into_iter() { assert_eq!(r.len(), 2); assert!(r[0].matches_type(ValueType::Ref)); assert!(r[1].matches_type(ValueType::Keyword)); } } else { panic!("Expected rel."); } println!("Rel took {}µs", (end - start).as_micros()); } #[test] fn test_failing_scalar() { let mut c = new_connection("").expect("Couldn't open conn."); let db = mentat_db::db::ensure_current_version(&mut c).expect("Couldn't open DB."); // Scalar that fails. let start = std::time::Instant::now(); let results = q_uncached( &c, &db.schema, "[:find ?x . :where [?x :db/fulltext true]]", None, ) .expect("Query failed") .results; let end = std::time::Instant::now(); assert_eq!(0, results.len()); if let QueryResults::Scalar(None) = results { } else { panic!("Expected failed scalar."); } println!("Failing scalar took {}µs", (end - start).as_micros()); } #[test] fn test_scalar() { let mut c = new_connection("").expect("Couldn't open conn."); let db = mentat_db::db::ensure_current_version(&mut c).expect("Couldn't open DB."); // Scalar that succeeds. let start = std::time::Instant::now(); let results = q_uncached( &c, &db.schema, "[:find ?ident . :where [24 :db/ident ?ident]]", None, ) .expect("Query failed") .results; let end = std::time::Instant::now(); assert_eq!(1, results.len()); if let QueryResults::Scalar(Some(Binding::Scalar(TypedValue::Keyword(ref rc)))) = results { // Should be '24'. assert_eq!(&Keyword::namespaced("db.type", "keyword"), rc.as_ref()); assert_eq!(KnownEntid(24), db.schema.get_entid(rc).unwrap()); } else { panic!("Expected scalar."); } println!("{:?}", results); println!("Scalar took {}µs", (end - start).as_micros()); } #[test] fn test_tuple() { let mut c = new_connection("").expect("Couldn't open conn."); let db = mentat_db::db::ensure_current_version(&mut c).expect("Couldn't open DB."); // Tuple. let start = std::time::Instant::now(); let results = q_uncached( &c, &db.schema, "[:find [?index ?cardinality] :where [:db/txInstant :db/index ?index] [:db/txInstant :db/cardinality ?cardinality]]", None, ) .expect("Query failed") .results; let end = std::time::Instant::now(); assert_eq!(1, results.len()); if let QueryResults::Tuple(Some(ref tuple)) = results { let cardinality_one = Keyword::namespaced("db.cardinality", "one"); assert_eq!(tuple.len(), 2); assert_eq!(tuple[0], TypedValue::Boolean(true).into()); assert_eq!( tuple[1], db.schema.get_entid(&cardinality_one).expect("c1").into() ); } else { panic!("Expected tuple."); } println!("{:?}", results); println!("Tuple took {}µs", (end - start).as_micros()); } #[test] fn test_coll() { let mut c = new_connection("").expect("Couldn't open conn."); let db = mentat_db::db::ensure_current_version(&mut c).expect("Couldn't open DB."); // Coll. let start = std::time::Instant::now(); let results = q_uncached( &c, &db.schema, "[:find [?e ...] :where [?e :db/ident _]]", None, ) .expect("Query failed") .results; let end = std::time::Instant::now(); assert_eq!(40, results.len()); if let QueryResults::Coll(ref coll) = results { assert!(coll.iter().all(|item| item.matches_type(ValueType::Ref))); } else { panic!("Expected coll."); } println!("{:?}", results); println!("Coll took {}µs", (end - start).as_micros()); } #[test] fn test_inputs() { let mut c = new_connection("").expect("Couldn't open conn."); let db = mentat_db::db::ensure_current_version(&mut c).expect("Couldn't open DB."); // entids::DB_INSTALL_VALUE_TYPE = 5. let ee = (Variable::from_valid_name("?e"), TypedValue::Ref(5)); let inputs = QueryInputs::with_value_sequence(vec![ee]); let results = q_uncached( &c, &db.schema, "[:find ?i . :in ?e :where [?e :db/ident ?i]]", inputs, ) .expect("query to succeed") .results; if let QueryResults::Scalar(Some(Binding::Scalar(TypedValue::Keyword(value)))) = results { assert_eq!( value.as_ref(), &Keyword::namespaced("db.install", "valueType") ); } else { panic!("Expected scalar."); } } /// Ensure that a query won't be run without all of its `:in` variables being bound. #[test] fn test_unbound_inputs() { let mut c = new_connection("").expect("Couldn't open conn."); let db = mentat_db::db::ensure_current_version(&mut c).expect("Couldn't open DB."); // Bind the wrong var by 'mistake'. let xx = (Variable::from_valid_name("?x"), TypedValue::Ref(5)); let inputs = QueryInputs::with_value_sequence(vec![xx]); let results = q_uncached( &c, &db.schema, "[:find ?i . :in ?e :where [?e :db/ident ?i]]", inputs, ); match results.expect_err("expected unbound variables") { MentatError::UnboundVariables(vars) => { assert_eq!(vars, vec!["?e".to_string()].into_iter().collect()); } _ => panic!("Expected UnboundVariables variant."), } } #[test] fn test_instants_and_uuids() { // We assume, perhaps foolishly, that the clocks on test machines won't lose more than an // hour while this test is running. let start = Utc::now() + FixedOffset::west_opt(60 * 60).unwrap(); let mut c = new_connection("").expect("Couldn't open conn."); let mut conn = Conn::connect(&mut c).expect("Couldn't open DB."); conn.transact( &mut c, r#"[ [:db/add "s" :db/ident :foo/uuid] [:db/add "s" :db/valueType :db.type/uuid] [:db/add "s" :db/cardinality :db.cardinality/one] ]"#, ) .unwrap(); conn.transact( &mut c, r#"[ [:db/add "u" :foo/uuid #uuid "cf62d552-6569-4d1b-b667-04703041dfc4"] ]"#, ) .unwrap(); let r = conn .q_once( &c, r#"[:find [?x ?u ?when] :where [?x :foo/uuid ?u ?tx] [?tx :db/txInstant ?when]]"#, None, ) .expect("results") .into(); match r { QueryResults::Tuple(Some(vals)) => { let mut vals = vals.into_iter(); match (vals.next(), vals.next(), vals.next(), vals.next()) { ( Some(Binding::Scalar(TypedValue::Ref(e))), Some(Binding::Scalar(TypedValue::Uuid(u))), Some(Binding::Scalar(TypedValue::Instant(t))), None, ) => { assert!(e > 40); // There are at least this many entities in the store. assert_eq!( Ok(u), Uuid::from_str("cf62d552-6569-4d1b-b667-04703041dfc4") ); assert!(t > start); } _ => panic!("Unexpected results."), } } _ => panic!("Expected query to work."), } } #[test] fn test_tx() { let mut c = new_connection("").expect("Couldn't open conn."); let mut conn = Conn::connect(&mut c).expect("Couldn't open DB."); conn.transact( &mut c, r#"[ [:db/add "s" :db/ident :foo/uuid] [:db/add "s" :db/valueType :db.type/uuid] [:db/add "s" :db/cardinality :db.cardinality/one] ]"#, ) .expect("successful transaction"); let t = conn .transact( &mut c, r#"[ [:db/add "u" :foo/uuid #uuid "cf62d552-6569-4d1b-b667-04703041dfc4"] ]"#, ) .expect("successful transaction"); conn.transact( &mut c, r#"[ [:db/add "u" :foo/uuid #uuid "550e8400-e29b-41d4-a716-446655440000"] ]"#, ) .expect("successful transaction"); let r = conn.q_once(&c, r#"[:find ?tx :where [?x :foo/uuid #uuid "cf62d552-6569-4d1b-b667-04703041dfc4" ?tx]]"#, None) .expect("results") .into(); match r { QueryResults::Rel(ref v) => { assert_eq!(*v, vec![vec![TypedValue::Ref(t.tx_id),]].into()); } _ => panic!("Expected query to work."), } } #[test] fn test_tx_as_input() { let mut c = new_connection("").expect("Couldn't open conn."); let mut conn = Conn::connect(&mut c).expect("Couldn't open DB."); conn.transact( &mut c, r#"[ [:db/add "s" :db/ident :foo/uuid] [:db/add "s" :db/valueType :db.type/uuid] [:db/add "s" :db/cardinality :db.cardinality/one] ]"#, ) .expect("successful transaction"); conn.transact( &mut c, r#"[ [:db/add "u" :foo/uuid #uuid "550e8400-e29b-41d4-a716-446655440000"] ]"#, ) .expect("successful transaction"); let t = conn .transact( &mut c, r#"[ [:db/add "u" :foo/uuid #uuid "cf62d552-6569-4d1b-b667-04703041dfc4"] ]"#, ) .expect("successful transaction"); conn.transact( &mut c, r#"[ [:db/add "u" :foo/uuid #uuid "267bab92-ee39-4ca2-b7f0-1163a85af1fb"] ]"#, ) .expect("successful transaction"); let tx = (Variable::from_valid_name("?tx"), TypedValue::Ref(t.tx_id)); let inputs = QueryInputs::with_value_sequence(vec![tx]); let r = conn .q_once( &c, r#"[:find ?uuid :in ?tx :where [?x :foo/uuid ?uuid ?tx]]"#, inputs, ) .expect("results") .into(); match r { QueryResults::Rel(ref v) => { assert_eq!( *v, vec![vec![TypedValue::Uuid( Uuid::from_str("cf62d552-6569-4d1b-b667-04703041dfc4").expect("Valid UUID") ),]] .into() ); } _ => panic!("Expected query to work."), } } #[test] fn test_fulltext() { let mut c = new_connection("").expect("Couldn't open conn."); let mut conn = Conn::connect(&mut c).expect("Couldn't open DB."); conn.transact( &mut c, r#"[ [:db/add "a" :db/ident :foo/term] [:db/add "a" :db/valueType :db.type/string] [:db/add "a" :db/fulltext false] [:db/add "a" :db/cardinality :db.cardinality/many] [:db/add "s" :db/ident :foo/fts] [:db/add "s" :db/valueType :db.type/string] [:db/add "s" :db/fulltext true] [:db/add "s" :db/index true] [:db/add "s" :db/cardinality :db.cardinality/many] ]"#, ) .unwrap(); let v = conn .transact( &mut c, r#"[ [:db/add "v" :foo/fts "hello darkness my old friend"] [:db/add "v" :foo/fts "I've come to talk with you again"] ]"#, ) .unwrap() .tempids .get("v") .cloned() .expect("v was mapped"); let r = conn .q_once( &c, r#"[:find [?x ?val ?score] :where [(fulltext $ :foo/fts "darkness") [[?x ?val _ ?score]]]]"#, None, ) .expect("results") .into(); match r { QueryResults::Tuple(Some(vals)) => { let mut vals = vals.into_iter(); match (vals.next(), vals.next(), vals.next(), vals.next()) { ( Some(Binding::Scalar(TypedValue::Ref(x))), Some(Binding::Scalar(TypedValue::String(text))), Some(Binding::Scalar(TypedValue::Double(score))), None, ) => { assert_eq!(x, v); assert_eq!(text.as_str(), "hello darkness my old friend"); assert_approx_eq!(score, OrderedFloat(0.0f64)); } _ => panic!("Unexpected results."), } } r => panic!("Unexpected results {:?}.", r), } let a = conn .transact(&mut c, r#"[[:db/add "a" :foo/term "talk"]]"#) .unwrap() .tempids .get("a") .cloned() .expect("a was mapped"); // If you use a non-constant search term, it must be bound earlier in the query. let query = r#"[:find ?x ?val :where [(fulltext $ :foo/fts ?term) [[?x ?val]]] [?a :foo/term ?term] ]"#; let r = conn.q_once(&c, query, None); match r.expect_err("expected query to fail") { MentatError::AlgebrizerError( query_algebrizer_traits::errors::AlgebrizerError::InvalidArgument( PlainSymbol(s), ty, i, ), ) => { assert_eq!(s, "fulltext"); assert_eq!(ty, "string"); assert_eq!(i, 2); } _ => panic!("Expected query to fail."), } // Bound to the wrong type? Error. let query = r#"[:find ?x ?val :where [?a :foo/term ?term] [(fulltext $ :foo/fts ?a) [[?x ?val]]]]"#; let r = conn.q_once(&c, query, None); match r.expect_err("expected query to fail") { MentatError::AlgebrizerError( query_algebrizer_traits::errors::AlgebrizerError::InvalidArgument( PlainSymbol(s), ty, i, ), ) => { assert_eq!(s, "fulltext"); assert_eq!(ty, "string"); assert_eq!(i, 2); } _ => panic!("Expected query to fail."), } // If it's bound, and the right type, it'll work! let query = r#"[:find ?x ?val :in ?a :where [?a :foo/term ?term] [(fulltext $ :foo/fts ?term) [[?x ?val]]]]"#; let inputs = QueryInputs::with_value_sequence(vec![( Variable::from_valid_name("?a"), TypedValue::Ref(a), )]); let r = conn.q_once(&c, query, inputs).expect("results").into(); match r { QueryResults::Rel(rels) => { let values: Vec> = rels.into_iter().collect(); assert_eq!( values, vec![vec![ Binding::Scalar(TypedValue::Ref(v)), "I've come to talk with you again".into(), ]] ); } _ => panic!("Expected query to work."), } } #[test] fn test_instant_range_query() { let mut c = new_connection("").expect("Couldn't open conn."); let mut conn = Conn::connect(&mut c).expect("Couldn't open DB."); conn.transact( &mut c, r#"[ [:db/add "a" :db/ident :foo/date] [:db/add "a" :db/valueType :db.type/instant] [:db/add "a" :db/cardinality :db.cardinality/one] ]"#, ) .unwrap(); let ids = conn .transact( &mut c, r#"[ [:db/add "b" :foo/date #inst "2016-01-01T11:00:00.000Z"] [:db/add "c" :foo/date #inst "2016-06-01T11:00:01.000Z"] [:db/add "d" :foo/date #inst "2017-01-01T11:00:02.000Z"] [:db/add "e" :foo/date #inst "2017-06-01T11:00:03.000Z"] ]"#, ) .unwrap() .tempids; let r = conn .q_once( &c, r#"[:find [?x ...] :order (asc ?date) :where [?x :foo/date ?date] [(< ?date #inst "2017-01-01T11:00:02.000Z")]]"#, None, ) .expect("results") .into(); match r { QueryResults::Coll(vals) => { assert_eq!( vals, vec![ Binding::Scalar(TypedValue::Ref(*ids.get("b").unwrap())), Binding::Scalar(TypedValue::Ref(*ids.get("c").unwrap())) ] ); } _ => panic!("Expected query to work."), } } #[test] fn test_lookup() { let mut c = new_connection("").expect("Couldn't open conn."); let mut conn = Conn::connect(&mut c).expect("Couldn't open DB."); conn.transact( &mut c, r#"[ [:db/add "a" :db/ident :foo/date] [:db/add "a" :db/valueType :db.type/instant] [:db/add "a" :db/cardinality :db.cardinality/one] [:db/add "b" :db/ident :foo/many] [:db/add "b" :db/valueType :db.type/long] [:db/add "b" :db/cardinality :db.cardinality/many] ]"#, ) .unwrap(); let ids = conn .transact( &mut c, r#"[ [:db/add "b" :foo/many 123] [:db/add "b" :foo/many 456] [:db/add "b" :foo/date #inst "2016-01-01T11:00:00.000Z"] [:db/add "c" :foo/date #inst "2016-06-01T11:00:01.000Z"] [:db/add "d" :foo/date #inst "2017-01-01T11:00:02.000Z"] [:db/add "e" :foo/date #inst "2017-06-01T11:00:03.000Z"] ]"#, ) .unwrap() .tempids; let entid = ids.get("b").unwrap(); let foo_date = kw!(:foo/date); let foo_many = kw!(:foo/many); let db_ident = kw!(:db/ident); let expected = TypedValue::Instant(DateTime::::from_str("2016-01-01T11:00:00.000Z").unwrap()); // Fetch a value. assert_eq!( expected, conn.lookup_value_for_attribute(&c, *entid, &foo_date) .unwrap() .unwrap() ); // Try to fetch a missing attribute. assert!(conn .lookup_value_for_attribute(&c, *entid, &db_ident) .unwrap() .is_none()); // Try to fetch from a non-existent entity. assert!(conn .lookup_value_for_attribute(&c, 12344567, &foo_date) .unwrap() .is_none()); // Fetch a multi-valued property. let two_longs = [TypedValue::Long(123), TypedValue::Long(456)]; let fetched_many = conn .lookup_value_for_attribute(&c, *entid, &foo_many) .unwrap() .unwrap(); assert!(two_longs.contains(&fetched_many)); } #[test] fn test_aggregates_type_handling() { let mut store = Store::open("").expect("opened"); store .transact( r#"[ {:db/ident :test/boolean :db/valueType :db.type/boolean :db/cardinality :db.cardinality/one} {:db/ident :test/long :db/valueType :db.type/long :db/cardinality :db.cardinality/one} {:db/ident :test/double :db/valueType :db.type/double :db/cardinality :db.cardinality/one} {:db/ident :test/string :db/valueType :db.type/string :db/cardinality :db.cardinality/one} {:db/ident :test/keyword :db/valueType :db.type/keyword :db/cardinality :db.cardinality/one} {:db/ident :test/uuid :db/valueType :db.type/uuid :db/cardinality :db.cardinality/one} {:db/ident :test/instant :db/valueType :db.type/instant :db/cardinality :db.cardinality/one} {:db/ident :test/ref :db/valueType :db.type/ref :db/cardinality :db.cardinality/one} ]"#, ) .unwrap(); store .transact( r#"[ {:test/boolean false :test/long 10 :test/double 2.4 :test/string "one" :test/keyword :foo/bar :test/uuid #uuid "55555234-1234-1234-1234-123412341234" :test/instant #inst "2017-01-01T11:00:00.000Z" :test/ref 1} {:test/boolean true :test/long 20 :test/double 4.4 :test/string "two" :test/keyword :foo/baz :test/uuid #uuid "66666234-1234-1234-1234-123412341234" :test/instant #inst "2018-01-01T11:00:00.000Z" :test/ref 2} {:test/boolean true :test/long 30 :test/double 6.4 :test/string "three" :test/keyword :foo/noo :test/uuid #uuid "77777234-1234-1234-1234-123412341234" :test/instant #inst "2019-01-01T11:00:00.000Z" :test/ref 3} ]"#, ) .unwrap(); // No type limits => can't do it. let r = store.q_once(r#"[:find (sum ?v) . :where [_ _ ?v]]"#, None); let all_types = ValueTypeSet::any(); match r.expect_err("expected query to fail") { MentatError::ProjectorError( ::query_projector_traits::errors::ProjectorError::CannotApplyAggregateOperationToTypes( SimpleAggregationOp::Sum, types, ), ) => { assert_eq!(types, all_types); } e => panic!("Unexpected error type {:?}", e), } // You can't sum instants. let r = store.q_once( r#"[:find (sum ?v) . :where [_ _ ?v] [(type ?v :db.type/instant)]]"#, None, ); match r.expect_err("expected query to fail") { MentatError::ProjectorError( ::query_projector_traits::errors::ProjectorError::CannotApplyAggregateOperationToTypes( SimpleAggregationOp::Sum, types, ), ) => { assert_eq!(types, ValueTypeSet::of_one(ValueType::Instant)); } e => panic!("Unexpected error type {:?}", e), } // But you can count them. let r = store .q_once( r#"[:find (count ?v) . :where [_ _ ?v] [(type ?v :db.type/instant)]]"#, None, ) .into_scalar_result() .expect("results") .unwrap(); // Our two transactions, the bootstrap transaction, plus the three values. assert_eq!(Binding::Scalar(TypedValue::Long(6)), r); // And you can min them, which returns an instant. let r = store .q_once( r#"[:find (min ?v) . :where [_ _ ?v] [(type ?v :db.type/instant)]]"#, None, ) .into_scalar_result() .expect("results") .unwrap(); let earliest = DateTime::parse_from_rfc3339("2017-01-01T11:00:00.000Z") .unwrap() .with_timezone(&Utc); assert_eq!(Binding::Scalar(TypedValue::Instant(earliest)), r); let r = store .q_once( r#"[:find (sum ?v) . :where [_ _ ?v] [(type ?v :db.type/long)]]"#, None, ) .into_scalar_result() .expect("results") .unwrap(); // Yes, the current version is in the store as a Long! let total = 30i64 + 20i64 + 10i64 + ::mentat_db::CORE_SCHEMA_VERSION as i64; assert_eq!(Binding::Scalar(TypedValue::Long(total)), r); let r = store .q_once( r#"[:find (avg ?v) . :where [_ _ ?v] [(type ?v :db.type/double)]]"#, None, ) .into_scalar_result() .expect("results") .unwrap(); let avg = (6.4f64 / 3f64) + (4.4f64 / 3f64) + (2.4f64 / 3f64); assert_eq!(Binding::Scalar(TypedValue::Double(avg.into())), r); } #[test] fn test_type_reqs() { let mut c = new_connection("").expect("Couldn't open conn."); let mut conn = Conn::connect(&mut c).expect("Couldn't open DB."); conn.transact( &mut c, r#"[ {:db/ident :test/boolean :db/valueType :db.type/boolean :db/cardinality :db.cardinality/one} {:db/ident :test/long :db/valueType :db.type/long :db/cardinality :db.cardinality/one} {:db/ident :test/double :db/valueType :db.type/double :db/cardinality :db.cardinality/one} {:db/ident :test/string :db/valueType :db.type/string :db/cardinality :db.cardinality/one} {:db/ident :test/keyword :db/valueType :db.type/keyword :db/cardinality :db.cardinality/one} {:db/ident :test/uuid :db/valueType :db.type/uuid :db/cardinality :db.cardinality/one} {:db/ident :test/instant :db/valueType :db.type/instant :db/cardinality :db.cardinality/one} {:db/ident :test/ref :db/valueType :db.type/ref :db/cardinality :db.cardinality/one} {:db/ident :test/bytes :db/valueType :db.type/bytes :db/cardinality :db.cardinality/one} ]"#, ) .unwrap(); conn.transact( &mut c, r#"[ {:test/boolean true :test/long 33 :test/double 1.4 :test/string "foo" :test/keyword :foo/bar :test/uuid #uuid "12341234-1234-1234-1234-123412341234" :test/instant #inst "2018-01-01T11:00:00.000Z" :test/ref 1 :test/bytes #bytes 010203050403022a } ]"#, ) .unwrap(); let eid_query = r#"[:find ?eid :where [?eid :test/string "foo"]]"#; let res = conn .q_once(&c, eid_query, None) .into_rel_result() .expect("results"); assert_eq!(res.row_count(), 1); assert_eq!(res.width, 1); let entid = match res.into_iter().next().unwrap().into_iter().next().unwrap() { Binding::Scalar(TypedValue::Ref(eid)) => eid, unexpected => { panic!( "Query to get the entity id returned unexpected result {:?}", unexpected ); } }; for value_type in ValueType::all_enums().iter() { let q = format!( "[:find [?v ...] :in ?e :where [?e _ ?v] [(type ?v {})]]", value_type.into_keyword() ); let results = conn .q_once( &c, &q, QueryInputs::with_value_sequence(vec![( Variable::from_valid_name("?e"), TypedValue::Ref(entid), )]), ) .expect("results") .into(); match results { QueryResults::Coll(vals) => { assert_eq!(vals.len(), 1, "Query should find exactly 1 item"); } v => { panic!("Query returned unexpected type: {:?}", v); } } } conn.transact( &mut c, r#"[ {:db/ident :test/long2 :db/valueType :db.type/long :db/cardinality :db.cardinality/one} ]"#, ) .unwrap(); conn.transact(&mut c, format!("[[:db/add {} :test/long2 5]]", entid)) .unwrap(); let longs_query = r#"[:find [?v ...] :order (asc ?v) :in ?e :where [?e _ ?v] [(type ?v :db.type/long)]]"#; let res = conn .q_once( &c, longs_query, QueryInputs::with_value_sequence(vec![( Variable::from_valid_name("?e"), TypedValue::Ref(entid), )]), ) .expect("results") .into(); match res { QueryResults::Coll(vals) => assert_eq!( vals, vec![ Binding::Scalar(TypedValue::Long(5)), Binding::Scalar(TypedValue::Long(33)) ] ), v => { panic!("Query returned unexpected type: {:?}", v); } }; } #[test] fn test_monster_head_aggregates() { let mut store = Store::open("").expect("opened"); let mut in_progress = store.begin_transaction().expect("began"); in_progress .transact( r#"[ {:db/ident :monster/heads :db/valueType :db.type/long :db/cardinality :db.cardinality/one} {:db/ident :monster/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/index true :db/unique :db.unique/identity} {:db/ident :monster/weapon :db/valueType :db.type/string :db/cardinality :db.cardinality/many} ]"#, ) .expect("transacted"); in_progress .transact( r#"[ {:monster/heads 1 :monster/name "Medusa" :monster/weapon "Stony gaze"} {:monster/heads 1 :monster/name "Cyclops" :monster/weapon ["Large club" "Mighty arms" "Stompy feet"]} {:monster/heads 1 :monster/name "Chimera" :monster/weapon "Goat-like agility"} {:monster/heads 3 :monster/name "Cerberus" :monster/weapon ["8-foot Kong®" "Deadly drool"]} ]"#, ) .expect("transacted"); // Without :with, uniqueness applies prior to aggregation, so we get 1 + 3 = 4. let res = in_progress .q_once( "[:find (sum ?heads) . :where [?monster :monster/heads ?heads]]", None, ) .expect("results") .into(); match res { QueryResults::Scalar(Some(Binding::Scalar(TypedValue::Long(count)))) => { assert_eq!(count, 4); } r => panic!("Unexpected result {:?}", r), }; // With :with, uniqueness includes the monster, so we get 1 + 1 + 1 + 3 = 6. let res = in_progress .q_once( "[:find (sum ?heads) . :with ?monster :where [?monster :monster/heads ?heads]]", None, ) .expect("results") .into(); match res { QueryResults::Scalar(Some(Binding::Scalar(TypedValue::Long(count)))) => { assert_eq!(count, 6); } r => panic!("Unexpected result {:?}", r), }; // Aggregates group. let res = in_progress .q_once( r#"[:find ?name (count ?weapon) :with ?monster :order (asc ?name) :where [?monster :monster/name ?name] [?monster :monster/weapon ?weapon]]"#, None, ) .expect("results") .into(); match res { QueryResults::Rel(vals) => { let expected = vec![ vec!["Cerberus".into(), TypedValue::Long(2)], vec!["Chimera".into(), TypedValue::Long(1)], vec!["Cyclops".into(), TypedValue::Long(3)], vec!["Medusa".into(), TypedValue::Long(1)], ]; assert_eq!(vals, expected.into()); } r => panic!("Unexpected result {:?}", r), }; in_progress.rollback().expect("rolled back"); } #[test] fn test_basic_aggregates() { let mut store = Store::open("").expect("opened"); store.transact(r#"[ {:db/ident :foo/is-vegetarian :db/valueType :db.type/boolean :db/cardinality :db.cardinality/one} {:db/ident :foo/age :db/valueType :db.type/long :db/cardinality :db.cardinality/one} {:db/ident :foo/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one} ]"#).unwrap(); let _ids = store .transact( r#"[ [:db/add "a" :foo/name "Alice"] [:db/add "b" :foo/name "Beli"] [:db/add "c" :foo/name "Carlos"] [:db/add "d" :foo/name "Diana"] [:db/add "a" :foo/is-vegetarian true] [:db/add "b" :foo/is-vegetarian true] [:db/add "c" :foo/is-vegetarian false] [:db/add "d" :foo/is-vegetarian false] [:db/add "a" :foo/age 14] [:db/add "b" :foo/age 22] [:db/add "c" :foo/age 42] [:db/add "d" :foo/age 28] ]"#, ) .unwrap() .tempids; // Count the number of distinct bindings of `?veg` that are `true` -- namely, one. // This is not the same as `count-distinct`: note the distinction between // including `:with` and not. // In this case, the `DISTINCT` must occur inside the aggregation, not outside it. /* Rather than: SELECT DISTINCT count(1) AS `(count ?veg)` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 65536 AND `datoms00`.v = 1; our query should be SELECT DISTINCT count(`?veg`) AS `(count ?veg)` FROM ( SELECT DISTINCT 1 AS `?veg` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 65536 AND `datoms00`.v = 1 ); */ let r = store .q_once( r#"[:find (count ?veg) :where [_ :foo/is-vegetarian ?veg] [(ground true) ?veg]]"#, None, ) .expect("results") .into(); match r { QueryResults::Rel(vals) => { assert_eq!(vals, vec![vec![TypedValue::Long(1)]].into()); } _ => panic!("Expected rel."), } // And this should be /* SELECT DISTINCT count(`?veg`) AS `(count ?veg)` FROM ( SELECT DISTINCT 1 AS `?veg`, `datoms00`.e AS `?person` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 65536 AND `datoms00`.v = 1 ); */ let r = store .q_once( r#"[:find (count ?veg) . :with ?person :where [?person :foo/is-vegetarian ?veg] [(ground true) ?veg]]"#, None, ) .expect("results") .into(); match r { QueryResults::Scalar(Some(val)) => { assert_eq!(val, Binding::Scalar(TypedValue::Long(2))); } _ => panic!("Expected scalar."), } // What are the oldest and youngest ages? let r = store .q_once( r#"[:find [(min ?age) (max ?age)] :where [_ :foo/age ?age]]"#, None, ) .expect("results") .into(); match r { QueryResults::Tuple(Some(vals)) => { assert_eq!( vals, vec![ Binding::Scalar(TypedValue::Long(14)), Binding::Scalar(TypedValue::Long(42)) ] ); } _ => panic!("Expected tuple."), } // Who's youngest, via order? let r = store .q_once( r#"[:find [?name ?age] :order (asc ?age) :where [?x :foo/age ?age] [?x :foo/name ?name]]"#, None, ) .expect("results") .into(); match r { QueryResults::Tuple(Some(vals)) => { assert_eq!( vals, vec!["Alice".into(), Binding::Scalar(TypedValue::Long(14))] ); } r => panic!("Unexpected results {:?}", r), } // Who's oldest, via order? let r = store .q_once( r#"[:find [?name ?age] :order (desc ?age) :where [?x :foo/age ?age] [?x :foo/name ?name]]"#, None, ) .expect("results") .into(); match r { QueryResults::Tuple(Some(vals)) => { assert_eq!( vals, vec!["Carlos".into(), Binding::Scalar(TypedValue::Long(42))] ); } _ => panic!("Expected tuple."), } // How many of each age do we have? // Add an extra person to make this interesting. store .transact(r#"[{:foo/name "Medusa", :foo/age 28}]"#) .expect("transacted"); // If we omit the 'with', we'll get the wrong answer: let r = store .q_once( r#"[:find ?age (count ?age) :order (asc ?age) :where [_ :foo/age ?age]]"#, None, ) .expect("results") .into(); match r { QueryResults::Rel(vals) => { assert_eq!( vals, vec![ vec![TypedValue::Long(14), TypedValue::Long(1)], vec![TypedValue::Long(22), TypedValue::Long(1)], vec![TypedValue::Long(28), TypedValue::Long(1)], vec![TypedValue::Long(42), TypedValue::Long(1)], ] .into() ); } _ => panic!("Expected rel."), } // If we include it, we'll get the right one: let r = store .q_once( r#"[:find ?age (count ?age) :with ?person :order (asc ?age) :where [?person :foo/age ?age]]"#, None, ) .expect("results") .into(); match r { QueryResults::Rel(vals) => { assert_eq!( vals, vec![ vec![TypedValue::Long(14), TypedValue::Long(1)], vec![TypedValue::Long(22), TypedValue::Long(1)], vec![TypedValue::Long(28), TypedValue::Long(2)], vec![TypedValue::Long(42), TypedValue::Long(1)], ] .into() ); } _ => panic!("Expected rel."), } } #[test] fn test_combinatorial() { let mut store = Store::open("").expect("opened"); store .transact( r#"[ [:db/add "a" :db/ident :foo/name] [:db/add "a" :db/valueType :db.type/string] [:db/add "a" :db/cardinality :db.cardinality/one] [:db/add "b" :db/ident :foo/dance] [:db/add "b" :db/valueType :db.type/ref] [:db/add "b" :db/cardinality :db.cardinality/many] [:db/add "b" :db/index true] ]"#, ) .unwrap(); store .transact( r#"[ [:db/add "a" :foo/name "Alice"] [:db/add "b" :foo/name "Beli"] [:db/add "c" :foo/name "Carlos"] [:db/add "d" :foo/name "Diana"] ;; Alice danced with Beli twice. [:db/add "a" :foo/dance "ab"] [:db/add "b" :foo/dance "ab"] [:db/add "a" :foo/dance "ba"] [:db/add "b" :foo/dance "ba"] ;; Carlos danced with Diana. [:db/add "c" :foo/dance "cd"] [:db/add "d" :foo/dance "cd"] ;; Alice danced with Diana. [:db/add "a" :foo/dance "ad"] [:db/add "d" :foo/dance "ad"] ]"#, ) .unwrap(); // How many different pairings of dancers were there? // If we just use `!=` (or `differ`), the number is doubled because of symmetry! assert_eq!( Binding::Scalar(TypedValue::Long(6)), store .q_once( r#"[:find (count ?right) . :with ?left :where [?left :foo/dance ?dance] [?right :foo/dance ?dance] [(differ ?left ?right)]]"#, None ) .into_scalar_result() .expect("scalar results") .unwrap() ); // SQL addresses this by using `<` instead of `!=` -- by imposing // an order on values, we can ensure that each pair only appears once, not // once per permutation. // It's far from ideal to expose an ordering on entids, because developers // will come to rely on it. Instead we expose a specific operator: `unpermute`. // When used in a query that generates permuted pairs of references, this // ensures that only one permutation is returned for a given pair. assert_eq!( Binding::Scalar(TypedValue::Long(3)), store .q_once( r#"[:find (count ?right) . :with ?left :where [?left :foo/dance ?dance] [?right :foo/dance ?dance] [(unpermute ?left ?right)]]"#, None ) .into_scalar_result() .expect("scalar results") .unwrap() ); } #[test] fn test_aggregate_the() { let mut store = Store::open("").expect("opened"); store .transact( r#"[ {:db/ident :visit/visitedOnDevice :db/valueType :db.type/ref :db/cardinality :db.cardinality/one} {:db/ident :visit/visitAt :db/valueType :db.type/instant :db/cardinality :db.cardinality/one} {:db/ident :site/visit :db/valueType :db.type/ref :db/isComponent true :db/cardinality :db.cardinality/many} {:db/ident :site/url :db/valueType :db.type/string :db/unique :db.unique/identity :db/cardinality :db.cardinality/one :db/index true} {:db/ident :visit/page :db/valueType :db.type/ref :db/isComponent true ; Debatable. :db/cardinality :db.cardinality/one} {:db/ident :page/title :db/valueType :db.type/string :db/fulltext true :db/index true :db/cardinality :db.cardinality/one} {:db/ident :visit/container :db/valueType :db.type/ref :db/cardinality :db.cardinality/one} ]"#, ) .expect("transacted schema"); store .transact( r#"[ {:db/ident :container/facebook} {:db/ident :container/personal} {:db/ident :device/my-desktop} ]"#, ) .expect("transacted idents"); store .transact( r#"[ {:visit/visitedOnDevice :device/my-desktop :visit/visitAt #inst "2018-04-06T20:46:00Z" :visit/container :container/facebook :db/id "another" :visit/page "fbpage2"} {:db/id "fbpage2" :page/title "(1) Facebook"} {:visit/visitedOnDevice :device/my-desktop :visit/visitAt #inst "2018-04-06T18:46:00Z" :visit/container :container/facebook :db/id "fbvisit" :visit/page "fbpage"} {:db/id "fbpage" :page/title "(2) Facebook"} {:site/url "https://www.facebook.com" :db/id "aa" :site/visit ["personalvisit" "another" "fbvisit"]} {:visit/visitedOnDevice :device/my-desktop :visit/visitAt #inst "2018-04-06T18:46:00Z" :visit/container :container/personal :db/id "personalvisit" :visit/page "personalpage"} {:db/id "personalpage" :page/title "Facebook - Log In or Sign Up"} ]"#, ) .expect("transacted data"); let per_title = store .q_once( r#" [:find (max ?visitDate) ?title :where [?site :site/url "https://www.facebook.com"] [?site :site/visit ?visit] [?visit :visit/container :container/facebook] [?visit :visit/visitAt ?visitDate] [?visit :visit/page ?page] [?page :page/title ?title]]"#, None, ) .into_rel_result() .expect("two results"); let corresponding_title = store .q_once( r#" [:find (max ?visitDate) (the ?title) :where [?site :site/url "https://www.facebook.com"] [?site :site/visit ?visit] [?visit :visit/container :container/facebook] [?visit :visit/visitAt ?visitDate] [?visit :visit/page ?page] [?page :page/title ?title]]"#, None, ) .into_rel_result() .expect("one result"); // This test shows the distinction between `?title` and `(the ?title`) — the former returns two // results, while the latter returns one. Without `the` we group by `?title`, getting the // maximum visit date for each title; with it we don't group by value, instead getting the title // that corresponds to the maximum visit date. // // 'Group' in this context translates to GROUP BY in the generated SQL. assert_eq!(2, per_title.row_count()); assert_eq!(1, corresponding_title.row_count()); assert_eq!( corresponding_title, vec![vec![ TypedValue::Instant(DateTime::::from_str("2018-04-06T20:46:00.000Z").unwrap()), TypedValue::typed_string("(1) Facebook") ]] .into() ); } // #249: min/max over refs, including transaction ids. #[test] fn test_min_max_ref() { let mut store = Store::open("").expect("opened"); store .transact( r#"[{:db/ident :t/r :db/valueType :db.type/ref :db/cardinality :db.cardinality/many} {:db/ident :t/n :db/valueType :db.type/long :db/cardinality :db.cardinality/one}]"#, ) .expect("schema"); let first = store.transact(r#"[{:db/id "a" :t/n 1}]"#).expect("a"); let a = *first.tempids.get("a").unwrap(); let last = store.transact(r#"[{:db/id "b" :t/n 2}]"#).expect("b"); let b = *last.tempids.get("b").unwrap(); store .transact(format!("[[:db/add {a} :t/r {a}] [:db/add {a} :t/r {b}]]").as_str()) .expect("refs"); let scalar = |q: &str| store.q_once(q, None).into_scalar_result().expect(q); assert_eq!( scalar("[:find (max ?tx) . :where [_ :t/n _ ?tx]]"), Some(Binding::Scalar(TypedValue::Ref(last.tx_id))) ); assert_eq!( scalar("[:find (min ?tx) . :where [_ :t/n _ ?tx]]"), Some(Binding::Scalar(TypedValue::Ref(first.tx_id))) ); assert_eq!( scalar("[:find (max ?v) . :where [_ :t/r ?v]]"), Some(Binding::Scalar(TypedValue::Ref(a.max(b)))) ); assert_eq!( scalar("[:find (min ?v) . :where [_ :t/r ?v]]"), Some(Binding::Scalar(TypedValue::Ref(a.min(b)))) ); // Grouped, in a relation. let rel = store .q_once("[:find ?e (max ?v) :where [?e :t/r ?v]]", None) .into_rel_result() .expect("rel"); assert_eq!( rel, vec![vec![TypedValue::Ref(a), TypedValue::Ref(a.max(b))]].into() ); // Sum over refs is still meaningless. assert!(store .q_once("[:find (sum ?v) . :where [_ :t/r ?v]]", None) .is_err()); } #[test] fn test_null_aggregates() { let store = Store::open("").expect("opened"); let rel = store .q_once( r#" [:find (count ?tx) (max ?txInstant) :where [_ _ _ ?tx] [?tx :db/txInstant ?txInstant] [(< ?txInstant #inst "2016-01-01T11:00:00.000Z")] ]"#, None, ) .into_rel_result() .expect("no results"); // (count ?tx) is 0, but (max ?txInstant) is over 0 SQL rows, yielding a NULL in the SQL rows. // We reject the entire row containing NULL aggregates. assert_eq!(0, rel.row_count()); let rel_pull = store .q_once( r#" [:find (count ?tx) (max ?txInstant) (pull ?tx [*]) :where [_ _ _ ?tx] [?tx :db/txInstant ?txInstant] [(< ?txInstant #inst "2016-01-01T11:00:00.000Z")] ]"#, None, ) .into_rel_result() .expect("no results"); // Same logic as above -- just verifying that `RelTwoStagePullProjector` handles NULL. assert_eq!(0, rel_pull.row_count()); let coll = store .q_once( r#" [:find [(max ?txInstant) ...] :where [_ _ _ ?tx] [?tx :db/txInstant ?txInstant] [(< ?txInstant #inst "2016-01-01T11:00:00.000Z")] ]"#, None, ) .into_coll_result() .expect("no results"); // (max ?txInstant) is over 0 SQL rows, yielding a NULL in the SQL rows. We reject the entire // row containing NULL aggregates, yielding an empty vector of results. assert_eq!(coll, vec![]); let tuple = store .q_once( r#" [:find [(count ?tx) (max ?txInstant)] :where [_ _ _ ?tx] [?tx :db/txInstant ?txInstant] [(< ?txInstant #inst "2016-01-01T11:00:00.000Z")] ]"#, None, ) .into_tuple_result() .expect("no results"); // (count ?tx) is 0, but (max ?txInstant) is over 0 SQL rows, yielding a NULL in the SQL rows. // We reject the entire row containing NULL aggregates, yielding no tuple result at all. assert_eq!(tuple, None); let tuple_pull = store .q_once( r#" [:find [(count ?tx) (max ?txInstant) (pull ?tx [*])] :where [_ _ _ ?tx] [?tx :db/txInstant ?txInstant] [(< ?txInstant #inst "2016-01-01T11:00:00.000Z")] ]"#, None, ) .into_tuple_result() .expect("no results"); // Same logic as above -- just verifying that `CollTwoStagePullProjector` handles NULL. assert_eq!(tuple_pull, None); let scalar = store .q_once( r#" [:find (max ?txInstant) . :where [_ _ _ ?tx] [?tx :db/txInstant ?txInstant] [(< ?txInstant #inst "2016-01-01T11:00:00.000Z")] ]"#, None, ) .into_scalar_result() .expect("no results"); // (max ?txInstant) is over 0 SQL rows, yielding a NULL in the SQL rows. We reject the entire // row containing NULL aggregates, yielding no scalar result at all. assert_eq!(scalar, None); } #[test] fn test_aggregation_implicit_grouping() { let mut store = Store::open("").expect("opened"); store .transact( r#"[ [:db/add "a" :db/ident :foo/score] [:db/add "a" :db/valueType :db.type/long] [:db/add "a" :db/cardinality :db.cardinality/one] [:db/add "b" :db/ident :foo/name] [:db/add "b" :db/valueType :db.type/string] [:db/add "b" :db/cardinality :db.cardinality/one] [:db/add "c" :db/ident :foo/is-vegetarian] [:db/add "c" :db/valueType :db.type/boolean] [:db/add "c" :db/cardinality :db.cardinality/one] [:db/add "d" :db/ident :foo/play] [:db/add "d" :db/valueType :db.type/ref] [:db/add "d" :db/cardinality :db.cardinality/many] [:db/add "d" :db/index true] [:db/add "d" :db/unique :db.unique/value] ]"#, ) .unwrap(); let ids = store .transact( r#"[ [:db/add "a" :foo/name "Alice"] [:db/add "b" :foo/name "Beli"] [:db/add "c" :foo/name "Carlos"] [:db/add "d" :foo/name "Diana"] [:db/add "a" :foo/is-vegetarian true] [:db/add "b" :foo/is-vegetarian true] [:db/add "c" :foo/is-vegetarian false] [:db/add "d" :foo/is-vegetarian false] [:db/add "aa" :foo/score 14] [:db/add "ab" :foo/score 99] [:db/add "ac" :foo/score 14] [:db/add "ba" :foo/score 22] [:db/add "bb" :foo/score 11] [:db/add "ca" :foo/score 42] [:db/add "da" :foo/score 5] [:db/add "db" :foo/score 28] [:db/add "d" :foo/play "da"] [:db/add "d" :foo/play "db"] [:db/add "a" :foo/play "aa"] [:db/add "a" :foo/play "ab"] [:db/add "a" :foo/play "ac"] [:db/add "b" :foo/play "ba"] [:db/add "b" :foo/play "bb"] [:db/add "c" :foo/play "ca"] ]"#, ) .unwrap() .tempids; // How many different scores were there? assert_eq!( Binding::Scalar(TypedValue::Long(7)), store .q_once( r#"[:find (count ?score) . :where [?game :foo/score ?score]]"#, None ) .into_scalar_result() .expect("scalar results") .unwrap() ); // How many different games resulted in scores? // '14' appears twice. assert_eq!( Binding::Scalar(TypedValue::Long(8)), store .q_once( r#"[:find (count ?score) . :with ?game :where [?game :foo/score ?score]]"#, None ) .into_scalar_result() .expect("scalar results") .unwrap() ); // Who's the highest-scoring vegetarian? assert_eq!( vec!["Alice".into(), Binding::Scalar(TypedValue::Long(99))], store .q_once( r#"[:find [(the ?name) (max ?score)] :where [?game :foo/score ?score] [?person :foo/play ?game] [?person :foo/is-vegetarian true] [?person :foo/name ?name]]"#, None ) .into_tuple_result() .expect("tuple results") .unwrap() ); // We can't run an ambiguous correspondence. let res = store.q_once( r#"[:find [(the ?name) (min ?score) (max ?score)] :where [?game :foo/score ?score] [?person :foo/play ?game] [?person :foo/is-vegetarian true] [?person :foo/name ?name]]"#, None, ); match res.expect_err("expected query to fail") { MentatError::ProjectorError( ::query_projector_traits::errors::ProjectorError::AmbiguousAggregates(mmc, cc), ) => { assert_eq!(mmc, 2); assert_eq!(cc, 1); } e => { panic!("Unexpected error type {:?}.", e); } } // Max scores for vegetarians. let expected: RelResult = vec![ vec!["Alice".into(), TypedValue::Long(99)], vec!["Beli".into(), TypedValue::Long(22)], ] .into(); assert_eq!( expected, store .q_once( r#"[:find ?name (max ?score) :where [?game :foo/score ?score] [?person :foo/play ?game] [?person :foo/is-vegetarian true] [?person :foo/name ?name]]"#, None ) .into_rel_result() .expect("rel results") ); // We can combine these aggregates. let r = store .q_once( r#"[:find ?x ?name (max ?score) (count ?score) (avg ?score) :with ?game ; So we don't discard duplicate scores! :where [?x :foo/name ?name] [?x :foo/play ?game] [?game :foo/score ?score]]"#, None, ) .expect("results") .into(); match r { QueryResults::Rel(vals) => { assert_eq!( vals, vec![ vec![ TypedValue::Ref(ids.get("a").cloned().unwrap()), "Alice".into(), TypedValue::Long(99), TypedValue::Long(3), TypedValue::Double((127f64 / 3f64).into()) ], vec![ TypedValue::Ref(ids.get("b").cloned().unwrap()), "Beli".into(), TypedValue::Long(22), TypedValue::Long(2), TypedValue::Double((33f64 / 2f64).into()) ], vec![ TypedValue::Ref(ids.get("c").cloned().unwrap()), "Carlos".into(), TypedValue::Long(42), TypedValue::Long(1), TypedValue::Double(42f64.into()) ], vec![ TypedValue::Ref(ids.get("d").cloned().unwrap()), "Diana".into(), TypedValue::Long(28), TypedValue::Long(2), TypedValue::Double((33f64 / 2f64).into()) ] ] .into() ); } x => panic!("Got unexpected results {:?}", x), } } #[test] fn test_tx_ids() { let mut store = Store::open("").expect("opened"); store .transact( r#"[ [:db/add "a" :db/ident :foo/term] [:db/add "a" :db/valueType :db.type/string] [:db/add "a" :db/fulltext false] [:db/add "a" :db/cardinality :db.cardinality/many] ]"#, ) .unwrap(); let tx1 = store .transact( r#"[ [:db/add "v" :foo/term "1"] ]"#, ) .expect("tx1 to apply") .tx_id; let tx2 = store .transact( r#"[ [:db/add "v" :foo/term "2"] ]"#, ) .expect("tx2 to apply") .tx_id; let tx3 = store .transact( r#"[ [:db/add "v" :foo/term "3"] ]"#, ) .expect("tx3 to apply") .tx_id; fn assert_tx_id_range(store: &Store, after: Entid, before: Entid, expected: Vec) { // TODO: after https://github.com/mozilla/mentat/issues/641, use q_prepare with inputs bound // at execution time. let r = store .q_once( r#"[:find [?tx ...] :in ?after ?before :where [(tx-ids $ ?after ?before) [?tx ...]] ]"#, QueryInputs::with_value_sequence(vec![ (Variable::from_valid_name("?after"), TypedValue::Ref(after)), ( Variable::from_valid_name("?before"), TypedValue::Ref(before), ), ]), ) .expect("results") .into(); match r { QueryResults::Coll(txs) => { let expected: Vec = expected.into_iter().map(|tv| tv.into()).collect(); assert_eq!(txs, expected); } x => panic!("Got unexpected results {:?}", x), } } assert_tx_id_range(&store, tx1, tx2, vec![TypedValue::Ref(tx1)]); assert_tx_id_range( &store, tx1, tx3, vec![TypedValue::Ref(tx1), TypedValue::Ref(tx2)], ); assert_tx_id_range(&store, tx2, tx3, vec![TypedValue::Ref(tx2)]); assert_tx_id_range( &store, tx2, tx3 + 1, vec![TypedValue::Ref(tx2), TypedValue::Ref(tx3)], ); } fn run_tx_data_test(mut store: Store) { store .transact( r#"[ [:db/add "a" :db/ident :foo/term] [:db/add "a" :db/valueType :db.type/string] [:db/add "a" :db/fulltext false] [:db/add "a" :db/cardinality :db.cardinality/many] ]"#, ) .unwrap(); let tx1 = store .transact( r#"[ [:db/add "e" :foo/term "1"] ]"#, ) .expect("tx1 to apply"); let tx2 = store .transact( r#"[ [:db/add "e" :foo/term "2"] ]"#, ) .expect("tx2 to apply"); fn assert_tx_data(store: &Store, tx: &TxReport, value: TypedValue) { // TODO: after https://github.com/mozilla/mentat/issues/641, use q_prepare with inputs bound // at execution time. let r = store .q_once( r#"[:find ?e ?a-name ?v ?tx ?added :in ?tx-in :where [(tx-data $ ?tx-in) [[?e ?a ?v ?tx ?added]]] [?a :db/ident ?a-name] :order ?e ]"#, QueryInputs::with_value_sequence(vec![( Variable::from_valid_name("?tx-in"), TypedValue::Ref(tx.tx_id), )]), ) .expect("results") .into(); let e = tx.tempids.get("e").cloned().expect("tempid"); match r { QueryResults::Rel(vals) => { assert_eq!( vals, vec![ vec![ TypedValue::Ref(e), TypedValue::typed_ns_keyword("foo", "term"), value, TypedValue::Ref(tx.tx_id), TypedValue::Boolean(true) ], vec![ TypedValue::Ref(tx.tx_id), TypedValue::typed_ns_keyword("db", "txInstant"), TypedValue::Instant(tx.tx_instant), TypedValue::Ref(tx.tx_id), TypedValue::Boolean(true) ], ] .into() ); } x => panic!("Got unexpected results {:?}", x), } } assert_tx_data(&store, &tx1, "1".into()); assert_tx_data(&store, &tx2, "2".into()); } #[test] fn test_tx_data() { run_tx_data_test(Store::open("").expect("opened")); } #[cfg(feature = "sqlcipher")] #[test] fn test_encrypted() { // We expect this to blow up completely if something is wrong with the encryption, // so the specific test we use doesn't matter that much. run_tx_data_test(Store::open_with_key("", "secret").expect("opened")); } // Ported from pg_mentat: named (non-recursive) rules, expanded by inlining. // A `grandparent` rule composed of two `parent` patterns. #[test] fn test_named_rule_expansion() { let mut store = Store::open("").expect("opened"); let (alice, _bob, carol) = { let mut ip = store.begin_transaction().expect("began"); ip.transact( r#"[{:db/ident :fam/parent :db/valueType :db.type/ref :db/cardinality :db.cardinality/many} {:db/ident :fam/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one}]"#, ) .expect("schema"); // Carol -> Bob -> Alice (parent points child->parent). let r = ip .transact( r#"[{:db/id "alice" :fam/name "Alice"} {:db/id "bob" :fam/name "Bob" :fam/parent "alice"} {:db/id "carol" :fam/name "Carol" :fam/parent "bob"}]"#, ) .expect("data"); let a = *r.tempids.get("alice").unwrap(); let b = *r.tempids.get("bob").unwrap(); let c = *r.tempids.get("carol").unwrap(); ip.commit().expect("committed"); (a, b, c) }; // grandparent(?c ?g) :- [?c :fam/parent ?p] [?p :fam/parent ?g] let q = r#"[:find ?cn ?gn :where (grandparent ?c ?g) [?c :fam/name ?cn] [?g :fam/name ?gn] :rules [[(grandparent ?c ?g) [?c :fam/parent ?p] [?p :fam/parent ?g]]]]"#; let results = store .begin_read() .expect("read") .q_once(q, None) .into_rel_result() .expect("results"); // Only Carol (grandchild) / Alice (grandparent). assert_eq!(results.row_count(), 1, "one grandparent pair"); let row = &results.rows().next().unwrap(); assert_eq!(row[0], Binding::Scalar(TypedValue::from("Carol"))); assert_eq!(row[1], Binding::Scalar(TypedValue::from("Alice"))); let _ = (alice, carol); } // Rule head variables bound to non-ref values, and rule invocations sharing // a variable (the pg_mentat rule cases fixed in 1.18.0): the inlined rule // gives the same rows as the hand-inlined query. pg_mentat's // rule_value_tests use these as its expected values. #[test] fn test_rule_values_and_shared_variables() { let mut store = Store::open("").expect("opened"); store .transact( r#"[{:db/ident :rv/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one} {:db/ident :rv/age :db/valueType :db.type/long :db/cardinality :db.cardinality/one} {:db/ident :rv/dbl :db/valueType :db.type/double :db/cardinality :db.cardinality/one} {:db/ident :rv/ok :db/valueType :db.type/boolean :db/cardinality :db.cardinality/one} {:db/ident :rv/kw :db/valueType :db.type/keyword :db/cardinality :db.cardinality/one} {:db/ident :rv/inst :db/valueType :db.type/instant :db/cardinality :db.cardinality/one} {:db/ident :rv/uuid :db/valueType :db.type/uuid :db/cardinality :db.cardinality/one} {:db/ident :rv/bytes :db/valueType :db.type/bytes :db/cardinality :db.cardinality/one} {:db/ident :rv/boss :db/valueType :db.type/ref :db/cardinality :db.cardinality/one}]"#, ) .expect("schema"); // ann (30) <- bob (12) <- cy (5) store .transact( r#"[{:db/id "a" :rv/name "ann" :rv/age 30 :rv/dbl 2.5 :rv/ok false :rv/kw :rv/k :rv/inst #inst "2024-01-15T10:30:00Z" :rv/uuid #uuid "550e8400-e29b-41d4-a716-446655440000" :rv/bytes #bytes 0a0b} {:db/id "b" :rv/name "bob" :rv/age 12 :rv/boss "a"} {:db/id "c" :rv/name "cy" :rv/age 5 :rv/boss "b"}]"#, ) .expect("data"); let rows = |q: &str| -> Vec> { let mut v: Vec> = store .q_once(q, None) .into_rel_result() .expect(q) .rows() .map(|r| r.to_vec()) .collect(); v.sort_by_key(|r| format!("{r:?}")); v }; for attr in [ "name", "age", "dbl", "ok", "kw", "inst", "uuid", "bytes", "boss", ] { let ruled = format!("[:find ?e ?v :rules [[(has ?e ?v) [?e :rv/{attr} ?v]]] :where (has ?e ?v)]"); let plain = format!("[:find ?e ?v :where [?e :rv/{attr} ?v]]"); let got = rows(&ruled); assert!(!got.is_empty(), "{ruled}"); assert_eq!(got, rows(&plain), "{ruled}"); } // Two rules sharing ?p: nobody is both an adult and a kid. let q = r#"[:find ?p :rules [[(adult ?p) [?p :rv/age ?a] [(>= ?a 18)]] [(kid ?p) [?p :rv/age ?a] [(< ?a 18)]]] :where (adult ?p) (kid ?p)]"#; assert!(rows(q).is_empty()); // The same rule twice, chained on ?y: cy's boss's boss is ann. let q = r#"[:find ?xn ?zn :rules [[(boss ?e ?b) [?e :rv/boss ?b]]] :where (boss ?x ?y) (boss ?y ?z) [?x :rv/name ?xn] [?z :rv/name ?zn]]"#; assert_eq!( rows(q), vec![vec![ Binding::Scalar(TypedValue::from("cy")), Binding::Scalar(TypedValue::from("ann")) ]] ); } // A recursive rule must produce a clear error (not a panic), pending WITH // RECURSIVE support in the SQL IR. #[test] fn test_recursive_rule_errors_cleanly() { let mut store = Store::open("").expect("opened"); let q = r#"[:find ?a ?d :where (ancestor ?a ?d) :rules [[(ancestor ?a ?d) [?a :fam/parent ?d]] [(ancestor ?a ?d) [?a :fam/parent ?x] (ancestor ?x ?d)]]]"#; let res = store.begin_read().expect("read").q_once(q, None); // Multi-clause (and recursive) => rejected, but as an error, not a panic. assert!(res.is_err(), "recursive/multi-clause rule should error"); } // A plain (non-namespaced) keyword in a pattern's value place is a keyword // constant, as in Datomic: `[?e :task/status :done]`. The shared `edn` grammar // used to reject plain keywords there; pg_mentat accepted them. #[test] fn test_plain_keyword_in_value_place() { let mut store = Store::open("").expect("opened"); store .transact( r#"[{:db/ident :task/status :db/valueType :db.type/keyword :db/cardinality :db.cardinality/one} {:db/ident :task/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one}]"#, ) .expect("schema"); store .transact( r#"[{:task/name "write" :task/status :done} {:task/name "test" :task/status :todo} {:task/name "ship" :task/status :task/blocked}]"#, ) .expect("data"); let mut names = |q: &str| -> Vec { let r = store .begin_read() .expect("read") .q_once(q, None) .expect("query") .into_coll() .expect("coll"); let mut v: Vec = r .into_iter() .map(|b| b.into_string().expect("string").to_string()) .collect(); v.sort(); v }; assert_eq!( names("[:find [?n ...] :where [?e :task/status :done] [?e :task/name ?n]]"), vec!["write"] ); assert_eq!( names("[:find [?n ...] :where [?e :task/status :todo] [?e :task/name ?n]]"), vec!["test"] ); // Namespaced keyword values still work. assert_eq!( names("[:find [?n ...] :where [?e :task/status :task/blocked] [?e :task/name ?n]]"), vec!["ship"] ); // A plain keyword nobody stored matches nothing (and is not an error). assert!(names("[:find [?n ...] :where [?e :task/status :nope] [?e :task/name ?n]]").is_empty()); } // #267: like / ilike and the string predicates. #[test] fn test_string_predicates() { let mut store = Store::open("").expect("opened"); store .transact( r#"[{:db/ident :s/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one} {:db/ident :s/pre :db/valueType :db.type/string :db/cardinality :db.cardinality/one} {:db/ident :s/kw :db/valueType :db.type/keyword :db/cardinality :db.cardinality/one} {:db/ident :s/n :db/valueType :db.type/long :db/cardinality :db.cardinality/one} {:db/ident :s/doc :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/fulltext true :db/index true}]"#, ) .expect("schema"); store .transact( r#"[{:s/name "Alice" :s/pre "Al" :s/kw :k/alpha :s/n 1 :s/doc "Alice in Wonderland"} {:s/name "alfred" :s/pre "xx" :s/n 2} {:s/name "50%" :s/pre "5"} {:s/name "500"} {:s/name "a*b?[c]"} {:s/name "axbyzc]"} {:s/name "café" :s/pre ""}]"#, ) .expect("data"); let names = |q: &str| -> Vec { let mut v: Vec = store .q_once(q, None) .into_coll_result() .unwrap_or_else(|e| panic!("{q}: {e}")) .into_iter() .map(|b| match b { Binding::Scalar(TypedValue::String(s)) => s.to_string(), Binding::Scalar(TypedValue::Keyword(k)) => k.to_string(), b => panic!("{b:?}"), }) .collect(); v.sort(); v }; let q = |pred: &str| names(&format!("[:find [?s ...] :where [_ :s/name ?s] [{pred}]]")); // like is case-sensitive; ilike is not. assert_eq!(q(r#"(like ?s "Al%")"#), vec!["Alice"]); assert_eq!(q(r#"(like ?s "al%")"#), vec!["alfred"]); assert_eq!(q(r#"(ilike ?s "AL%")"#), vec!["Alice", "alfred"]); assert_eq!(q(r#"(like ?s "_lice")"#), vec!["Alice"]); // \ escapes % and _; GLOB metacharacters in a pattern are literals. assert_eq!(q(r#"(like ?s "50\\%")"#), vec!["50%"]); assert_eq!(q(r#"(like ?s "50%")"#), vec!["50%", "500"]); assert_eq!(q(r#"(like ?s "a*b?[c]")"#), vec!["a*b?[c]"]); assert_eq!(q(r#"(ilike ?s "A*B?[C]")"#), vec!["a*b?[c]"]); assert_eq!(q(r#"(like ?s "caf_")"#), vec!["café"]); // starts-with? / ends-with? / includes?, with constants. assert_eq!(q(r#"(starts-with? ?s "Al")"#), vec!["Alice"]); assert_eq!(q(r#"(ends-with? ?s "é")"#), vec!["café"]); assert_eq!(q(r#"(includes? ?s "*b?")"#), vec!["a*b?[c]"]); assert_eq!(q(r#"(includes? ?s "%")"#), vec!["50%"]); assert_eq!(q(r#"(ends-with? ?s "")"#).len(), 7); // ... and with a bound second argument. assert_eq!( names("[:find [?s ...] :where [?e :s/name ?s] [?e :s/pre ?p] [(starts-with? ?s ?p)]]"), vec!["50%", "Alice", "café"] ); // A pattern bound with :in. let inputs = QueryInputs::with_value_sequence(vec![( Variable::from_valid_name("?p"), TypedValue::typed_string("%e%"), )]); let mut v: Vec<_> = store .q_once( "[:find [?s ...] :in ?p :where [_ :s/name ?s] [(like ?s ?p)]]", inputs, ) .into_coll_result() .expect("like with :in"); v.sort_by_key(|b| format!("{b:?}")); assert_eq!( v, vec![ Binding::Scalar(TypedValue::typed_string("Alice")), Binding::Scalar(TypedValue::typed_string("alfred")) ] ); // A keyword attribute matches on its text without the leading `:`, as on pg_mentat. assert_eq!( names(r#"[:find [?v ...] :where [_ :s/kw ?v] [(like ?v "k/%")]]"#), vec![":k/alpha"] ); assert_eq!( names(r#"[:find [?v ...] :where [_ :s/kw ?v] [(starts-with? ?v ":")]]"#), Vec::::new() ); // An untyped value: only strings match, other types are skipped. assert_eq!( names(r#"[:find [?v ...] :where [?e :s/n _] [?e ?a ?v] [(like ?v "Al%")]]"#), vec!["Al", "Alice", "Alice in Wonderland"] ); // A fulltext attribute matches on its text. assert_eq!( names(r#"[:find [?v ...] :where [_ :s/doc ?v] [(ilike ?v "%wonder%")]]"#), vec!["Alice in Wonderland"] ); // Inside not. assert_eq!( names(r#"[:find [?s ...] :where [?e :s/name ?s] [?e :s/n _] (not [(like ?s "A%")])]"#), vec!["alfred"] ); // Errors: arity, a pattern from a column, a trailing escape, a non-string pattern. for (pred, msg) in [ (r#"(like ?s)"#, "invalid number of arguments to like: expected 2, got 1."), (r#"(like ?s ?s)"#, "invalid argument to like: expected constant string pattern in position 1."), (r#"(like ?s "a\\")"#, "invalid argument to like: expected pattern that does not end with an escape character in position 1."), (r#"(starts-with? ?s 5)"#, "invalid argument to starts-with?: expected string in position 1."), ] { let err = store .q_once(&format!("[:find ?s :where [_ :s/name ?s] [{pred}]]"), None) .expect_err(pred); assert_eq!(err.to_string(), msg, "{pred}"); } } // #281: the TxReport counts the datoms a transaction asserted and retracted. #[test] fn test_tx_report_counts() { let mut store = Store::open("").expect("opened"); let counts = |r: &TxReport| (r.datoms_asserted.unwrap(), r.datoms_retracted.unwrap()); let r = store .transact( r#"[{:db/ident :c/n :db/valueType :db.type/long :db/cardinality :db.cardinality/one} {:db/ident :c/m :db/valueType :db.type/long :db/cardinality :db.cardinality/many} {:db/ident :c/s :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/fulltext true :db/index true}]"#, ) .expect("schema"); assert_eq!(counts(&r), (11, 0)); let r = store .transact(r#"[{:db/id "a" :c/n 1 :c/m [1 2] :c/s "x"}]"#) .expect("a"); assert_eq!(counts(&r), (4, 0)); let a = *r.tempids.get("a").unwrap(); for (tx, want) in [ // Re-asserting what is there, and retracting what isn't, change nothing. ( format!("[[:db/add {a} :c/n 1] [:db/add {a} :c/m 2] [:db/add {a} :c/s \"x\"]]"), (0, 0), ), ( format!("[[:db/retract {a} :c/n 7] [:db/retract {a} :c/m 9]]"), (0, 0), ), ("[]".to_string(), (0, 0)), // Replacing a cardinality-one value: one assertion, one retraction. (format!("[[:db/add {a} :c/n 2]]"), (1, 1)), (format!("[[:db/add {a} :c/s \"y\"]]"), (1, 1)), ( format!("[[:db/add {a} :c/m 3] [:db/retract {a} :c/m 1]]"), (1, 1), ), (format!("[[:db/retractEntity {a}]]"), (0, 4)), ] { let r = store.transact(tx.as_str()).expect(&tx); assert_eq!(counts(&r), want, "{tx}"); assert_eq!(r.is_empty(), want == (0, 0), "{tx}"); } // A :db/noHistory replacement retracts the old value like any other (pg_mentat agrees). store .transact( "[{:db/ident :c/h :db/valueType :db.type/long :db/cardinality :db.cardinality/one :db/noHistory true}]", ) .expect("noHistory"); let r = store.transact(r#"[{:db/id "b" :c/h 1}]"#).expect("b"); let b = *r.tempids.get("b").unwrap(); let r = store.transact(&format!("[[:db/add {b} :c/h 2]]")).unwrap(); assert_eq!(counts(&r), (1, 1)); } // #123: a non-namespaced keyword value reads back (it transacted, then failed to read). #[test] fn test_plain_keyword_value_round_trips() { let mut store = Store::open("").expect("opened"); store .transact( r#"[{:db/ident :t/k :db/valueType :db.type/keyword :db/cardinality :db.cardinality/many}]"#, ) .expect("schema"); store .transact(r#"[{:db/id "a" :t/k [:plain :ns/name]}]"#) .expect("data"); let mut ks: Vec = store .q_once("[:find [?k ...] :where [_ :t/k ?k]]", None) .into_coll_result() .expect("read back") .into_iter() .map(|b| match b { Binding::Scalar(TypedValue::Keyword(k)) => k.to_string(), b => panic!("{b:?}"), }) .collect(); ks.sort(); assert_eq!(ks, vec![":ns/name", ":plain"]); // As a query constant, and through pull. assert!(store .q_once("[:find ?e . :where [?e :t/k :plain]]", None) .into_scalar_result() .expect("constant") .is_some()); let pulled = store .q_once("[:find (pull ?e [:t/k]) . :where [?e :t/k :plain]]", None) .into_scalar_result() .expect("pull"); assert!(format!("{pulled:?}").contains("plain"), "{pulled:?}"); } // #179: a comparison over a value of unknown type applies to comparable values only. #[test] fn test_inequality_over_untyped_values() { let mut store = Store::open("").expect("opened"); store .transact( r#"[{:db/ident :u/id :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/unique :db.unique/identity :db/index true} {:db/ident :u/n :db/valueType :db.type/long :db/cardinality :db.cardinality/one} {:db/ident :u/d :db/valueType :db.type/double :db/cardinality :db.cardinality/one} {:db/ident :u/b :db/valueType :db.type/boolean :db/cardinality :db.cardinality/one} {:db/ident :u/r :db/valueType :db.type/ref :db/cardinality :db.cardinality/one} {:db/ident :u/t :db/valueType :db.type/instant :db/cardinality :db.cardinality/one}]"#, ) .expect("schema"); store .transact( r#"[{:db/id "x" :u/id "e1" :u/n 5 :u/d 2.5 :u/b true :u/r "x" :u/t #inst "2020-01-01T00:00:00Z"}]"#, ) .expect("data"); let vals = |q: &str| -> Vec { let mut v: Vec = store .q_once(q, None) .into_coll_result() .unwrap_or_else(|e| panic!("{q}: {e}")) .iter() .map(|b| format!("{b:?}")) .collect(); v.sort(); v }; // Longs and doubles only: not the string, the ref, the boolean or the instant. assert_eq!( vals(r#"[:find [?v ...] :where [?e :u/id "e1"] [?e ?a ?v] [(> ?v 1)]]"#), ["Scalar(Double(2.5))", "Scalar(Long(5))"] ); assert_eq!( vals(r#"[:find [?v ...] :where [?e :u/id "e1"] [?e ?a ?v] [(<= ?v 2.5)]]"#), ["Scalar(Double(2.5))"] ); // An instant constant compares with instants only. assert_eq!( vals(r#"[:find [?v ...] :where [?e :u/id "e1"] [?e ?a ?v] [(< ?v #inst "2021-01-01T00:00:00Z")]]"#).len(), 1 ); // Over the whole store: no error (the bootstrap has keywords, strings, refs...). assert!(store .q_once("[:find ?v :where [_ ?a ?v] [(> ?v 4)]]", None) .is_ok()); // A typed attribute is unchanged. assert_eq!( vals(r#"[:find [?v ...] :where [_ :u/n ?v] [(> ?v 1)]]"#), ["Scalar(Long(5))"] ); } // #145 (Mozilla): a fulltext datom stores the rowid of its text in fulltext_values, so a // bare integer in the value place must not match it by that rowid. #[test] fn test_unattributed_integer_does_not_match_fulltext_rowid() { let mut store = Store::open("").expect("opened"); store .transact( r#"[{:db/ident :ft/text :db/valueType :db.type/string :db/cardinality :db.cardinality/many :db/fulltext true :db/index true} {:db/ident :ft/n :db/valueType :db.type/long :db/cardinality :db.cardinality/one}]"#, ) .expect("schema"); // 50 texts on one entity, so every rowid in 1..=50 is one of its values. let texts: Vec = (1..=50).map(|i| format!("\"text {i}\"")).collect(); let report = store .transact(&format!( "[{{:db/id \"t\" :ft/text [{}]}} {{:db/id \"n\" :ft/n 42}}]", texts.join(" ") )) .expect("data"); let (t, n) = (report.tempids["t"], report.tempids["n"]); let rowids: Vec = store .sqlite_ref() .prepare("SELECT d.v FROM datoms d WHERE d.e = ? AND d.index_fulltext") .unwrap() .query_map([t], |r| r.get(0)) .unwrap() .collect::>() .unwrap(); assert!(rowids.contains(&42), "{rowids:?}"); let matching = |store: &mut Store, v: i64| -> Vec { store .q_once(&format!("[:find [?x ...] :where [?x _ {v}]]"), None) .into_coll_result() .expect("query") }; let found = matching(&mut store, 42); assert!( found.contains(&Binding::Scalar(TypedValue::Ref(n))), "{found:?}" ); for v in rowids { let found = matching(&mut store, v); assert!( !found.contains(&Binding::Scalar(TypedValue::Ref(t))), "[?x _ {v}]: {found:?}" ); } } // #237: an aggregate over no rows is nil, as in Datomic, for every find spec; a count is 0. // (A grouped aggregate has no groups, so no rows.) #[test] fn test_aggregates_over_no_rows_are_nil() { let mut store = Store::open("").expect("opened"); store .transact( r#"[{:db/ident :z/n :db/valueType :db.type/long :db/cardinality :db.cardinality/one} {:db/ident :z/d :db/valueType :db.type/double :db/cardinality :db.cardinality/one} {:db/ident :z/t :db/valueType :db.type/instant :db/cardinality :db.cardinality/one} {:db/ident :z/k :db/valueType :db.type/long :db/cardinality :db.cardinality/one}]"#, ) .expect("schema"); store.transact("[{:z/k 1}]").expect("data"); let run = |store: &mut Store, q: String| { store .q_once(&q, None) .unwrap_or_else(|e| panic!("{q}: {e}")) .results }; let zero = Binding::Scalar(TypedValue::Long(0)); for (agg, attrs) in [ ("sum", &[":z/n", ":z/d"][..]), ("avg", &[":z/n", ":z/d"]), ("min", &[":z/n", ":z/d", ":z/t"]), ("max", &[":z/n", ":z/d", ":z/t"]), ("count", &[":z/n", ":z/d", ":z/t"]), ] { let count = agg == "count"; for attr in attrs { let r = run( &mut store, format!("[:find ({agg} ?v) . :where [_ {attr} ?v]]"), ); let want = QueryResults::Scalar(count.then(|| zero.clone())); assert_eq!(r, want, "{agg} {attr} scalar"); let r = run( &mut store, format!("[:find [({agg} ?v) ...] :where [_ {attr} ?v]]"), ); let want = QueryResults::Coll(if count { vec![zero.clone()] } else { vec![] }); assert_eq!(r, want, "{agg} {attr} coll"); let r = run( &mut store, format!("[:find [({agg} ?v)] :where [_ {attr} ?v]]"), ); let want = QueryResults::Tuple(count.then(|| vec![zero.clone()])); assert_eq!(r, want, "{agg} {attr} tuple"); let r = run( &mut store, format!("[:find ({agg} ?v) :where [_ {attr} ?v]]"), ); assert_eq!(r.len(), count as usize, "{agg} {attr} rel: {r:?}"); let r = run( &mut store, format!("[:find ?k ({agg} ?v) :where [?e :z/k ?k] [?e {attr} ?v]]"), ); assert!(r.is_empty(), "{agg} {attr} grouped rel: {r:?}"); } } // With a nil aggregate in the find there is no row, even beside a count. let r = run( &mut store, "[:find [(sum ?v) (count ?v)] :where [_ :z/n ?v]]".into(), ); assert_eq!(r, QueryResults::Tuple(None)); // Over some rows they still aggregate. store.transact("[{:z/n 5}]").expect("data"); assert_eq!( store .q_once("[:find [(sum ?v) (count ?v)] :where [_ :z/n ?v]]", None) .into_tuple_result() .expect("tuple"), Some(vec![ Binding::Scalar(TypedValue::Long(5)), Binding::Scalar(TypedValue::Long(1)) ]) ); }