From bc63744abadeab4319b7fc70ec5834dcbf134507 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Wed, 19 Apr 2017 16:16:19 -0700 Subject: [PATCH] Add :limit to queries (#420) r=nalexander * Pre: put query parts in alphabetical order. * Pre: rename 'input' to 'query' in translate tests. * Part 1: parse :limit. * Part 2: validate and escape variable parameters in SQL. * Part 3: algebrize and translate limits. --- query-algebrizer/src/clauses/mod.rs | 5 + query-algebrizer/src/errors.rs | 5 + query-algebrizer/src/lib.rs | 77 +++++++++---- query-parser/src/parse.rs | 121 ++++++++++++++++---- query-parser/tests/find_tests.rs | 41 ++++++- query-projector/src/lib.rs | 9 +- query-sql/Cargo.toml | 2 + query-sql/src/lib.rs | 36 +++++- query-translator/src/translate.rs | 20 ++-- query-translator/tests/translate.rs | 167 ++++++++++++++++++++-------- query/src/lib.rs | 12 +- sql/src/lib.rs | 2 +- src/conn.rs | 11 +- src/query.rs | 12 +- tests/query.rs | 14 +-- 15 files changed, 394 insertions(+), 140 deletions(-) diff --git a/query-algebrizer/src/clauses/mod.rs b/query-algebrizer/src/clauses/mod.rs index 188411c5..ca054d89 100644 --- a/query-algebrizer/src/clauses/mod.rs +++ b/query-algebrizer/src/clauses/mod.rs @@ -441,6 +441,11 @@ impl ConjoiningClauses { QueryValue::PrimitiveLong(value))) } + /// Mark the given value as a long. + pub fn constrain_var_to_long(&mut self, variable: Variable) { + self.narrow_types_for_var(variable, unit_type_set(ValueType::Long)); + } + /// Mark the given value as one of the set of numeric types. fn constrain_var_to_numeric(&mut self, variable: Variable) { let mut numeric_types = HashSet::with_capacity(2); diff --git a/query-algebrizer/src/errors.rs b/query-algebrizer/src/errors.rs index 256ee970..2434bd3c 100644 --- a/query-algebrizer/src/errors.rs +++ b/query-algebrizer/src/errors.rs @@ -47,6 +47,11 @@ error_chain! { display("invalid argument to {}: expected numeric in position {}.", function, position) } + InvalidLimit(val: String, kind: ValueType) { + description("invalid limit") + display("invalid limit {} of type {}: expected natural number.", val, kind) + } + NonMatchingVariablesInOrClause { // TODO: flesh out. description("non-matching variables in 'or' clause") diff --git a/query-algebrizer/src/lib.rs b/query-algebrizer/src/lib.rs index 29b8ad5f..4315a6dc 100644 --- a/query-algebrizer/src/lib.rs +++ b/query-algebrizer/src/lib.rs @@ -24,6 +24,8 @@ mod clauses; use mentat_core::{ Schema, + TypedValue, + ValueType, }; use mentat_core::counter::RcCounter; @@ -31,6 +33,7 @@ use mentat_core::counter::RcCounter; use mentat_query::{ FindQuery, FindSpec, + Limit, Order, SrcVar, Variable, @@ -53,28 +56,11 @@ pub struct AlgebraicQuery { has_aggregates: bool, pub with: BTreeSet, pub order: Option>, - pub limit: Option, + pub limit: Limit, pub cc: clauses::ConjoiningClauses, } impl AlgebraicQuery { - /** - * Apply a new limit to this query, if one is provided and any existing limit is larger. - */ - pub fn apply_limit(&mut self, limit: Option) { - match self.limit { - None => self.limit = limit, - Some(existing) => - match limit { - None => (), - Some(new) => - if new < existing { - self.limit = limit; - }, - }, - }; - } - #[inline] pub fn is_known_empty(&self) -> bool { self.cc.is_known_empty() @@ -131,6 +117,45 @@ fn validate_and_simplify_order(cc: &ConjoiningClauses, order: Option> } } + +fn simplify_limit(mut query: AlgebraicQuery) -> Result { + // Unpack any limit variables in place. + let refined_limit = + match query.limit { + Limit::Variable(ref v) => { + match query.cc.bound_value(v) { + Some(TypedValue::Long(n)) => { + if n <= 0 { + // User-specified limits should always be natural numbers (> 0). + bail!(ErrorKind::InvalidLimit(n.to_string(), ValueType::Long)); + } else { + Some(Limit::Fixed(n as u64)) + } + }, + Some(val) => { + // Same. + bail!(ErrorKind::InvalidLimit(format!("{:?}", val), val.value_type())); + }, + None => { + // We know that the limit variable is mentioned in `:in`. + // That it's not bound here implies that we haven't got all the variables + // we'll need to run the query yet. + // (We should never hit this in `q_once`.) + // Simply pass the `Limit` through to `SelectQuery` untouched. + None + }, + } + }, + Limit::None => None, + Limit::Fixed(_) => None, + }; + + if let Some(lim) = refined_limit { + query.limit = lim; + } + Ok(query) +} + pub fn algebrize_with_inputs(schema: &Schema, parsed: FindQuery, counter: usize, @@ -138,6 +163,11 @@ pub fn algebrize_with_inputs(schema: &Schema, let alias_counter = RcCounter::with_initial(counter); let mut cc = ConjoiningClauses::with_inputs_and_alias_counter(parsed.in_vars, inputs, alias_counter); + // Do we have a variable limit? If so, tell the CC that the var must be numeric. + if let &Limit::Variable(ref var) = &parsed.limit { + cc.constrain_var_to_long(var.clone()); + } + // TODO: integrate default source into pattern processing. // TODO: flesh out the rest of find-into-context. let where_clauses = parsed.where_clauses; @@ -149,8 +179,10 @@ pub fn algebrize_with_inputs(schema: &Schema, let (order, extra_vars) = validate_and_simplify_order(&cc, parsed.order)?; let with: BTreeSet = parsed.with.into_iter().chain(extra_vars.into_iter()).collect(); - let limit = if parsed.find_spec.is_unit_limited() { Some(1) } else { None }; - Ok(AlgebraicQuery { + + // This might leave us with an unused `:in` variable. + let limit = if parsed.find_spec.is_unit_limited() { Limit::Fixed(1) } else { parsed.limit }; + let q = AlgebraicQuery { default_source: parsed.default_source, find_spec: parsed.find_spec, has_aggregates: false, // TODO: we don't parse them yet. @@ -158,7 +190,10 @@ pub fn algebrize_with_inputs(schema: &Schema, order: order, limit: limit, cc: cc, - }) + }; + + // Substitute in any fixed values and fail if they're out of range. + simplify_limit(q) } pub use clauses::{ diff --git a/query-parser/src/parse.rs b/query-parser/src/parse.rs index 3017b797..f7e73c1d 100644 --- a/query-parser/src/parse.rs +++ b/query-parser/src/parse.rs @@ -18,7 +18,7 @@ use std; // To refer to std::result::Result. use std::collections::BTreeSet; use self::combine::{eof, many, many1, optional, parser, satisfy, satisfy_map, Parser, ParseResult, Stream}; -use self::combine::combinator::{choice, or, try}; +use self::combine::combinator::{any, choice, or, try}; use self::mentat_parser_utils::{ ResultParser, @@ -43,6 +43,7 @@ use self::mentat_query::{ FindSpec, FnArg, FromValue, + Limit, Order, OrJoin, OrWhereClause, @@ -102,6 +103,16 @@ error_chain! { description("missing field") display("missing field: '{}'", value) } + + UnknownLimitVar(var: edn::PlainSymbol) { + description("limit var not present in :in") + display("limit var {} not present in :in", var) + } + + InvalidLimit(val: edn::Value) { + description("limit value not valid") + display("expected natural number, got {}", val) + } } } @@ -156,6 +167,20 @@ def_parser!(Where, pattern_value_place, PatternValuePlace, { satisfy_map(PatternValuePlace::from_value) }); +def_parser!(Query, natural_number, u64, { + any().and_then(|v: edn::ValueAndSpan| { + match v.inner { + edn::SpannedValue::Integer(x) if (x > 0) => { + Ok(x as u64) + }, + spanned => { + let e = Box::new(Error::from_kind(ErrorKind::InvalidLimit(spanned.into()))); + Err(combine::primitives::Error::Other(e)) + }, + } + }) +}); + def_parser!(Where, pattern_non_value_place, PatternNonValuePlace, { satisfy_map(PatternNonValuePlace::from_value) }); @@ -334,22 +359,20 @@ def_parser!(Find, spec, FindSpec, { }); def_matches_keyword!(Find, literal_find, "find"); - def_matches_keyword!(Find, literal_in, "in"); - -def_matches_keyword!(Find, literal_with, "with"); - -def_matches_keyword!(Find, literal_where, "where"); - +def_matches_keyword!(Find, literal_limit, "limit"); def_matches_keyword!(Find, literal_order, "order"); +def_matches_keyword!(Find, literal_where, "where"); +def_matches_keyword!(Find, literal_with, "with"); /// Express something close to a builder pattern for a `FindQuery`. enum FindQueryPart { FindSpec(FindSpec), - With(BTreeSet), In(BTreeSet), - WhereClauses(Vec), + Limit(Limit), Order(Vec), + WhereClauses(Vec), + With(BTreeSet), } def_parser!(Find, vars, BTreeSet, { @@ -373,49 +396,72 @@ def_parser!(Find, query, FindQuery, { let p_find_spec = Find::literal_find() .with(vector().of_exactly(Find::spec().map(FindQueryPart::FindSpec))); - let p_in_vars = Find::literal_in().with(Find::vars().map(FindQueryPart::In)); + let p_in_vars = Find::literal_in() + .with(Find::vars().map(FindQueryPart::In)); - let p_with_vars = Find::literal_with().with(Find::vars().map(FindQueryPart::With)); - - let p_where_clauses = Find::literal_where() - .with(vector().of_exactly(Where::clauses().map(FindQueryPart::WhereClauses))).expected(":where clauses"); + let p_limit = Find::literal_limit() + .with(vector().of_exactly( + Query::variable().map(|v| Limit::Variable(v)) + .or(Query::natural_number().map(|n| Limit::Fixed(n))))) + .map(FindQueryPart::Limit); let p_order_clauses = Find::literal_order() .with(vector().of_exactly(many1(Query::order()).map(FindQueryPart::Order))); + let p_where_clauses = Find::literal_where() + .with(vector().of_exactly(Where::clauses().map(FindQueryPart::WhereClauses))) + .expected(":where clauses"); + + let p_with_vars = Find::literal_with() + .with(Find::vars().map(FindQueryPart::With)); + (or(map(), keyword_map())) - .of_exactly(many(choice::<[&mut Parser; 5], _>([ + .of_exactly(many(choice::<[&mut Parser; 6], _>([ + // Ordered by likelihood. &mut try(p_find_spec), - &mut try(p_in_vars), - &mut try(p_with_vars), &mut try(p_where_clauses), + &mut try(p_in_vars), + &mut try(p_limit), &mut try(p_order_clauses), + &mut try(p_with_vars), ]))) .and_then(|parts: Vec| -> std::result::Result> { let mut find_spec = None; let mut in_vars = None; - let mut with_vars = None; - let mut where_clauses = None; + let mut limit = Limit::None; let mut order_clauses = None; + let mut where_clauses = None; + let mut with_vars = None; for part in parts { match part { FindQueryPart::FindSpec(x) => find_spec = Some(x), - FindQueryPart::With(x) => with_vars = Some(x), FindQueryPart::In(x) => in_vars = Some(x), - FindQueryPart::WhereClauses(x) => where_clauses = Some(x), + FindQueryPart::Limit(x) => limit = x, FindQueryPart::Order(x) => order_clauses = Some(x), + FindQueryPart::WhereClauses(x) => where_clauses = Some(x), + FindQueryPart::With(x) => with_vars = Some(x), + } + } + + // Make sure that if we have `:limit ?x`, `?x` appears in `:in`. + let in_vars = in_vars.unwrap_or(BTreeSet::default()); + if let Limit::Variable(ref v) = limit { + if !in_vars.contains(v) { + let e = Box::new(Error::from_kind(ErrorKind::UnknownLimitVar(v.name()))); + return Err(combine::primitives::Error::Other(e)); } } Ok(FindQuery { - find_spec: find_spec.clone().ok_or(combine::primitives::Error::Unexpected("expected :find".into()))?, default_source: SrcVar::DefaultSrc, - with: with_vars.unwrap_or(BTreeSet::default()), - in_vars: in_vars.unwrap_or(BTreeSet::default()), + find_spec: find_spec.clone().ok_or(combine::primitives::Error::Unexpected("expected :find".into()))?, in_sources: BTreeSet::default(), // TODO + in_vars: in_vars, + limit: limit, order: order_clauses, where_clauses: where_clauses.ok_or(combine::primitives::Error::Unexpected("expected :where".into()))?, + with: with_vars.unwrap_or(BTreeSet::default()), }) }) }); @@ -675,4 +721,31 @@ mod test { FindSpec::FindTuple(vec![Element::Variable(variable(vx)), Element::Variable(variable(vy))])); } + + #[test] + fn test_natural_numbers() { + let text = edn::Value::Text("foo".to_string()); + let neg = edn::Value::Integer(-10); + let zero = edn::Value::Integer(0); + let pos = edn::Value::Integer(5); + + // This is terrible, but destructuring errors is a shitshow. + let mut par = Query::natural_number(); + let x = par.parse(text.with_spans().into_atom_stream()).err().expect("an error").errors; + let result = format!("{:?}", x); + assert_eq!(result, "[Other(Error(InvalidLimit(Text(\"foo\")), State { next_error: None, backtrace: None })), Expected(Borrowed(\"natural_number\"))]"); + + let mut par = Query::natural_number(); + let x = par.parse(neg.with_spans().into_atom_stream()).err().expect("an error").errors; + let result = format!("{:?}", x); + assert_eq!(result, "[Other(Error(InvalidLimit(Integer(-10)), State { next_error: None, backtrace: None })), Expected(Borrowed(\"natural_number\"))]"); + + let mut par = Query::natural_number(); + let x = par.parse(zero.with_spans().into_atom_stream()).err().expect("an error").errors; + let result = format!("{:?}", x); + assert_eq!(result, "[Other(Error(InvalidLimit(Integer(0)), State { next_error: None, backtrace: None })), Expected(Borrowed(\"natural_number\"))]"); + + let mut par = Query::natural_number(); + assert_eq!(None, par.parse(pos.with_spans().into_atom_stream()).err()); + } } diff --git a/query-parser/tests/find_tests.rs b/query-parser/tests/find_tests.rs index 00d09afd..7e577fdd 100644 --- a/query-parser/tests/find_tests.rs +++ b/query-parser/tests/find_tests.rs @@ -8,9 +8,9 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. -extern crate mentat_query_parser; -extern crate mentat_query; extern crate edn; +extern crate mentat_query; +extern crate mentat_query_parser; use edn::{ NamespacedKeyword, @@ -22,6 +22,7 @@ use mentat_query::{ Element, FindSpec, FnArg, + Limit, Order, OrJoin, OrWhereClause, @@ -123,7 +124,7 @@ fn can_parse_unit_or_join() { #[test] fn can_parse_simple_or_join() { - let s = "[:find ?x . :where (or-join [?x] [?x _ 10] [?x _ 15])]"; + let s = "[:find ?x . :where (or-join [?x] [?x _ 10] [?x _ -15])]"; let p = parse_find_string(s).unwrap(); assert_eq!(p.find_spec, @@ -146,7 +147,7 @@ fn can_parse_simple_or_join() { source: None, entity: PatternNonValuePlace::Variable(Variable::from_valid_name("?x")), attribute: PatternNonValuePlace::Placeholder, - value: PatternValuePlace::EntidOrInteger(15), + value: PatternValuePlace::EntidOrInteger(-15), tx: PatternNonValuePlace::Placeholder, })), ], @@ -234,3 +235,35 @@ fn can_parse_order_by() { Some(vec![Order(Direction::Descending, Variable::from_valid_name("?y")), Order(Direction::Ascending, Variable::from_valid_name("?x"))])); } + +#[test] +fn can_parse_limit() { + let invalid = "[:find ?x :where [?x :foo/baz ?y] :limit]"; + assert!(parse_find_string(invalid).is_err()); + + let zero_invalid = "[:find ?x :where [?x :foo/baz ?y] :limit 00]"; + assert!(parse_find_string(zero_invalid).is_err()); + + let none = "[:find ?x :where [?x :foo/baz ?y]]"; + assert_eq!(parse_find_string(none).unwrap().limit, + Limit::None); + + let one = "[:find ?x :where [?x :foo/baz ?y] :limit 1]"; + assert_eq!(parse_find_string(one).unwrap().limit, + Limit::Fixed(1)); + + let onethousand = "[:find ?x :where [?x :foo/baz ?y] :limit 1000]"; + assert_eq!(parse_find_string(onethousand).unwrap().limit, + Limit::Fixed(1000)); + + let variable_with_in = "[:find ?x :in ?limit :where [?x :foo/baz ?y] :limit ?limit]"; + assert_eq!(parse_find_string(variable_with_in).unwrap().limit, + Limit::Variable(Variable::from_valid_name("?limit"))); + + let variable_with_in_used = "[:find ?x :in ?limit :where [?x :foo/baz ?limit] :limit ?limit]"; + assert_eq!(parse_find_string(variable_with_in_used).unwrap().limit, + Limit::Variable(Variable::from_valid_name("?limit"))); + + let variable_without_in = "[:find ?x :where [?x :foo/baz ?y] :limit ?limit]"; + assert!(parse_find_string(variable_without_in).is_err()); +} diff --git a/query-projector/src/lib.rs b/query-projector/src/lib.rs index 0e9e19ed..358667e5 100644 --- a/query-projector/src/lib.rs +++ b/query-projector/src/lib.rs @@ -37,6 +37,7 @@ use mentat_db::{ use mentat_query::{ Element, FindSpec, + Limit, Variable, }; @@ -442,8 +443,8 @@ pub struct CombinedProjection { } impl CombinedProjection { - fn flip_distinct_for_limit(mut self, limit: Option) -> Self { - if limit == Some(1) { + fn flip_distinct_for_limit(mut self, limit: &Limit) -> Self { + if *limit == Limit::Fixed(1) { self.distinct = false; } self @@ -474,7 +475,7 @@ pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection { match query.find_spec { FindColl(ref element) => { let (cols, templates) = project_elements(1, iter::once(element), query); - CollProjector::combine(cols, templates).flip_distinct_for_limit(query.limit) + CollProjector::combine(cols, templates).flip_distinct_for_limit(&query.limit) }, FindScalar(ref element) => { @@ -485,7 +486,7 @@ pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection { FindRel(ref elements) => { let column_count = query.find_spec.expected_column_count(); let (cols, templates) = project_elements(column_count, elements, query); - RelProjector::combine(column_count, cols, templates).flip_distinct_for_limit(query.limit) + RelProjector::combine(column_count, cols, templates).flip_distinct_for_limit(&query.limit) }, FindTuple(ref elements) => { diff --git a/query-sql/Cargo.toml b/query-sql/Cargo.toml index cd5ed968..7b951f11 100644 --- a/query-sql/Cargo.toml +++ b/query-sql/Cargo.toml @@ -4,6 +4,8 @@ version = "0.0.1" workspace = ".." [dependencies] +regex = "0.2" + [dependencies.mentat_core] path = "../core" diff --git a/query-sql/src/lib.rs b/query-sql/src/lib.rs index 86ff8636..28fb2ddc 100644 --- a/query-sql/src/lib.rs +++ b/query-sql/src/lib.rs @@ -8,6 +8,7 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. +extern crate regex; extern crate mentat_core; extern crate mentat_query; extern crate mentat_query_algebrizer; @@ -20,6 +21,8 @@ use mentat_core::{ use mentat_query::{ Direction, + Limit, + Variable, }; use mentat_query_algebrizer::{ @@ -158,7 +161,7 @@ pub struct SelectQuery { pub from: FromClause, pub constraints: Vec, pub order: Vec, - pub limit: Option, + pub limit: Limit, } fn push_variable_column(qb: &mut QueryBuilder, vc: &VariableColumn) -> BuildQueryResult { @@ -401,6 +404,19 @@ impl QueryFragment for FromClause { } } +impl SelectQuery { + fn push_variable_param(&self, var: &Variable, out: &mut QueryBuilder) -> BuildQueryResult { + // `var` is something like `?foo99-people`. + // Trim the `?` and escape the rest. Prepend `i` to distinguish from + // the inline value space `v`. + let re = regex::Regex::new("[^a-zA-Z_0-9]").unwrap(); + let without_question = var.as_str().split_at(1).1; + let replaced = re.replace_all(without_question, "_"); + let bind_param = format!("i{}", replaced); // We _could_ avoid this copying. + out.push_bind_param(bind_param.as_str()) + } +} + impl QueryFragment for SelectQuery { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { if self.distinct { @@ -430,10 +446,18 @@ impl QueryFragment for SelectQuery { { out.push_sql(", ") }); } - // Guaranteed to be positive: u64. - if let Some(limit) = self.limit { - out.push_sql(" LIMIT "); - out.push_sql(limit.to_string().as_str()); + match &self.limit { + &Limit::None => (), + &Limit::Fixed(limit) => { + // Guaranteed to be non-negative: u64. + out.push_sql(" LIMIT "); + out.push_sql(limit.to_string().as_str()); + }, + &Limit::Variable(ref var) => { + // Guess this wasn't bound yet. Produce an argument. + out.push_sql(" LIMIT "); + self.push_variable_param(var, out)?; + }, } Ok(()) @@ -554,7 +578,7 @@ mod tests { }, ], order: vec![], - limit: None, + limit: Limit::None, }; let SQLQuery { sql, args } = query.to_sql_query().unwrap(); diff --git a/query-translator/src/translate.rs b/query-translator/src/translate.rs index 9161ac53..6de63955 100644 --- a/query-translator/src/translate.rs +++ b/query-translator/src/translate.rs @@ -14,6 +14,8 @@ use mentat_core::{ ValueType, }; +use mentat_query::Limit; + use mentat_query_algebrizer::{ AlgebraicQuery, ColumnAlternation, @@ -224,7 +226,7 @@ fn table_for_computed(computed: ComputedTable, alias: TableAlias) -> TableOrSubq // Each arm simply turns into a subquery. // The SQL translation will stuff "UNION" between each arm. let projection = Projection::Columns(columns); - cc_to_select_query(projection, cc, false, None, None) + cc_to_select_query(projection, cc, false, None, Limit::None) }).collect(), alias) }, @@ -234,11 +236,11 @@ fn table_for_computed(computed: ComputedTable, alias: TableAlias) -> TableOrSubq /// Returns a `SelectQuery` that queries for the provided `cc`. Note that this _always_ returns a /// query that runs SQL. The next level up the call stack can check for known-empty queries if /// needed. -fn cc_to_select_query(projection: Projection, - cc: ConjoiningClauses, - distinct: bool, - order: Option>, - limit: T) -> SelectQuery where T: Into> { +fn cc_to_select_query(projection: Projection, + cc: ConjoiningClauses, + distinct: bool, + order: Option>, + limit: Limit) -> SelectQuery { let from = if cc.from.is_empty() { FromClause::Nothing } else { @@ -268,7 +270,7 @@ fn cc_to_select_query(projection: Projection, // Turn the query-centric order clauses into column-orders. let order = order.map_or(vec![], |vec| { vec.into_iter().map(|o| o.into()).collect() }); - let limit = if cc.empty_because.is_some() { Some(0) } else { limit.into() }; + let limit = if cc.empty_because.is_some() { Limit::Fixed(0) } else { limit }; SelectQuery { distinct: distinct, projection: projection, @@ -293,10 +295,10 @@ pub fn cc_to_exists(cc: ConjoiningClauses) -> SelectQuery { from: FromClause::Nothing, constraints: vec![], order: vec![], - limit: Some(0), + limit: Limit::Fixed(0), } } else { - cc_to_select_query(Projection::One, cc, false, None, 1) + cc_to_select_query(Projection::One, cc, false, None, Limit::Fixed(1)) } } diff --git a/query-translator/tests/translate.rs b/query-translator/tests/translate.rs index 452cce0d..9d533b69 100644 --- a/query-translator/tests/translate.rs +++ b/query-translator/tests/translate.rs @@ -17,17 +17,25 @@ extern crate mentat_sql; use std::rc::Rc; -use mentat_query::NamespacedKeyword; +use mentat_query::{ + NamespacedKeyword, + Variable, +}; use mentat_core::{ Attribute, Entid, Schema, + TypedValue, ValueType, }; use mentat_query_parser::parse_find_string; -use mentat_query_algebrizer::algebrize; +use mentat_query_algebrizer::{ + QueryInputs, + algebrize, + algebrize_with_inputs, +}; use mentat_query_translator::{ query_to_select, }; @@ -43,14 +51,17 @@ fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) { schema.schema_map.insert(e, a); } -fn translate>>(schema: &Schema, input: &'static str, limit: T) -> SQLQuery { - let parsed = parse_find_string(input).expect("parse failed"); - let mut algebrized = algebrize(schema, parsed).expect("algebrize failed"); - algebrized.apply_limit(limit.into()); +fn translate_with_inputs(schema: &Schema, query: &'static str, inputs: QueryInputs) -> SQLQuery { + let parsed = parse_find_string(query).expect("parse failed"); + let algebrized = algebrize_with_inputs(schema, parsed, 0, inputs).expect("algebrize failed"); let select = query_to_select(algebrized); select.query.to_sql_query().unwrap() } +fn translate(schema: &Schema, query: &'static str) -> SQLQuery { + translate_with_inputs(schema, query, QueryInputs::default()) +} + fn prepopulated_typed_schema(foo_type: ValueType) -> Schema { let mut schema = Schema::default(); associate_ident(&mut schema, NamespacedKeyword::new("foo", "bar"), 99); @@ -73,8 +84,8 @@ fn make_arg(name: &'static str, value: &'static str) -> (String, Rc) { fn test_scalar() { let schema = prepopulated_schema(); - let input = r#"[:find ?x . :where [?x :foo/bar "yyy"]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x . :where [?x :foo/bar "yyy"]]"#; + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 1"); assert_eq!(args, vec![make_arg("$v0", "yyy")]); } @@ -83,8 +94,8 @@ fn test_scalar() { fn test_tuple() { let schema = prepopulated_schema(); - let input = r#"[:find [?x] :where [?x :foo/bar "yyy"]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find [?x] :where [?x :foo/bar "yyy"]]"#; + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 1"); assert_eq!(args, vec![make_arg("$v0", "yyy")]); } @@ -93,8 +104,8 @@ fn test_tuple() { fn test_coll() { let schema = prepopulated_schema(); - let input = r#"[:find [?x ...] :where [?x :foo/bar "yyy"]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find [?x ...] :where [?x :foo/bar "yyy"]]"#; + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0"); assert_eq!(args, vec![make_arg("$v0", "yyy")]); } @@ -103,8 +114,8 @@ fn test_coll() { fn test_rel() { let schema = prepopulated_schema(); - let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#; + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0"); assert_eq!(args, vec![make_arg("$v0", "yyy")]); } @@ -113,18 +124,80 @@ fn test_rel() { fn test_limit() { let schema = prepopulated_schema(); - let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#; - let SQLQuery { sql, args } = translate(&schema, input, 5); + let query = r#"[:find ?x :where [?x :foo/bar "yyy"] :limit 5]"#; + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 5"); assert_eq!(args, vec![make_arg("$v0", "yyy")]); } +#[test] +fn test_unbound_variable_limit() { + let schema = prepopulated_schema(); + + // We don't know the value of the limit var, so we produce an escaped SQL variable to handle + // later input. + let query = r#"[:find ?x :in ?limit-is-9-great :where [?x :foo/bar "yyy"] :limit ?limit-is-9-great]"#; + let SQLQuery { sql, args } = translate_with_inputs(&schema, query, QueryInputs::default()); + assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` \ + FROM `datoms` AS `datoms00` \ + WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 \ + LIMIT $ilimit_is_9_great"); + assert_eq!(args, vec![make_arg("$v0", "yyy")]); +} + +#[test] +fn test_bound_variable_limit() { + let schema = prepopulated_schema(); + + // We know the value of `?limit` at algebrizing time, so we substitute directly. + let query = r#"[:find ?x :in ?limit :where [?x :foo/bar "yyy"] :limit ?limit]"#; + let inputs = QueryInputs::with_value_sequence(vec![(Variable::from_valid_name("?limit"), TypedValue::Long(92))]); + let SQLQuery { sql, args } = translate_with_inputs(&schema, query, inputs); + assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 92"); + assert_eq!(args, vec![make_arg("$v0", "yyy")]); +} + +#[test] +fn test_bound_variable_limit_affects_distinct() { + let schema = prepopulated_schema(); + + // We know the value of `?limit` at algebrizing time, so we substitute directly. + // As it's `1`, we know we don't need `DISTINCT`! + let query = r#"[:find ?x :in ?limit :where [?x :foo/bar "yyy"] :limit ?limit]"#; + let inputs = QueryInputs::with_value_sequence(vec![(Variable::from_valid_name("?limit"), TypedValue::Long(1))]); + let SQLQuery { sql, args } = translate_with_inputs(&schema, query, inputs); + assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 1"); + assert_eq!(args, vec![make_arg("$v0", "yyy")]); +} + +#[test] +fn test_bound_variable_limit_affects_types() { + let schema = prepopulated_schema(); + + let query = r#"[:find ?x ?limit :in ?limit :where [?x _ ?limit] :limit ?limit]"#; + let parsed = parse_find_string(query).expect("parse failed"); + let algebrized = algebrize(&schema, parsed).expect("algebrize failed"); + + // The type is known. + assert_eq!(Some(ValueType::Long), + algebrized.cc.known_type(&Variable::from_valid_name("?limit"))); + + let select = query_to_select(algebrized); + let SQLQuery { sql, args } = select.query.to_sql_query().unwrap(); + + // TODO: this query isn't actually correct -- we don't yet algebrize for variables that are + // specified in `:in` but not provided at algebrizing time. But it shows what we care about + // at the moment: we don't project a type column, because we know it's a Long. + assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x`, `datoms00`.v AS `?limit` FROM `datoms` AS `datoms00` LIMIT $ilimit"); + assert_eq!(args, vec![]); +} + #[test] fn test_unknown_attribute_keyword_value() { let schema = Schema::default(); - let input = r#"[:find ?x :where [?x _ :ab/yyy]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x :where [?x _ :ab/yyy]]"#; + let SQLQuery { sql, args } = translate(&schema, query); // Only match keywords, not strings: tag = 13. assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = $v0 AND `datoms00`.value_type_tag = 13"); @@ -135,8 +208,8 @@ fn test_unknown_attribute_keyword_value() { fn test_unknown_attribute_string_value() { let schema = Schema::default(); - let input = r#"[:find ?x :where [?x _ "horses"]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x :where [?x _ "horses"]]"#; + let SQLQuery { sql, args } = translate(&schema, query); // We expect all_datoms because we're querying for a string. Magic, that. // We don't want keywords etc., so tag = 10. @@ -148,8 +221,8 @@ fn test_unknown_attribute_string_value() { fn test_unknown_attribute_double_value() { let schema = Schema::default(); - let input = r#"[:find ?x :where [?x _ 9.95]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x :where [?x _ 9.95]]"#; + let SQLQuery { sql, args } = translate(&schema, query); // In general, doubles _could_ be 1.0, which might match a boolean or a ref. Set tag = 5 to // make sure we only match numbers. @@ -167,22 +240,22 @@ fn test_unknown_attribute_integer_value() { let two = r#"[:find ?x :where [?x _ 2]]"#; // Can't match boolean; no need to filter it out. - let SQLQuery { sql, args } = translate(&schema, negative, None); + let SQLQuery { sql, args } = translate(&schema, negative); assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = -1"); assert_eq!(args, vec![]); // Excludes booleans. - let SQLQuery { sql, args } = translate(&schema, zero, None); + let SQLQuery { sql, args } = translate(&schema, zero); assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE (`datoms00`.v = 0 AND `datoms00`.value_type_tag <> 1)"); assert_eq!(args, vec![]); // Excludes booleans. - let SQLQuery { sql, args } = translate(&schema, one, None); + let SQLQuery { sql, args } = translate(&schema, one); assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE (`datoms00`.v = 1 AND `datoms00`.value_type_tag <> 1)"); assert_eq!(args, vec![]); // Can't match boolean; no need to filter it out. - let SQLQuery { sql, args } = translate(&schema, two, None); + let SQLQuery { sql, args } = translate(&schema, two); assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = 2"); assert_eq!(args, vec![]); } @@ -208,8 +281,8 @@ fn test_unknown_ident() { fn test_numeric_less_than_unknown_attribute() { let schema = Schema::default(); - let input = r#"[:find ?x :where [?x _ ?y] [(< ?y 10)]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x :where [?x _ ?y] [(< ?y 10)]]"#; + let SQLQuery { sql, args } = translate(&schema, query); // Although we infer numericness from numeric predicates, we've already assigned a table to the // first pattern, and so this is _still_ `all_datoms`. @@ -220,8 +293,8 @@ fn test_numeric_less_than_unknown_attribute() { #[test] fn test_numeric_gte_known_attribute() { let schema = prepopulated_typed_schema(ValueType::Double); - let input = r#"[:find ?x :where [?x :foo/bar ?y] [(>= ?y 12.9)]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x :where [?x :foo/bar ?y] [(>= ?y 12.9)]]"#; + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v >= 12.9"); assert_eq!(args, vec![]); } @@ -229,8 +302,8 @@ fn test_numeric_gte_known_attribute() { #[test] fn test_numeric_not_equals_known_attribute() { let schema = prepopulated_typed_schema(ValueType::Long); - let input = r#"[:find ?x . :where [?x :foo/bar ?y] [(!= ?y 12)]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x . :where [?x :foo/bar ?y] [(!= ?y 12)]]"#; + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v <> 12 LIMIT 1"); assert_eq!(args, vec![]); } @@ -248,14 +321,14 @@ fn test_simple_or_join() { }); } - let input = r#"[:find [?url ?description] + let query = r#"[:find [?url ?description] :where (or-join [?page] [?page :page/url "http://foo.com/"] [?page :page/title "Foo"]) [?page :page/url ?url] [?page :page/description ?description]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT `datoms01`.v AS `?url`, `datoms02`.v AS `?description` FROM `datoms` AS `datoms00`, `datoms` AS `datoms01`, `datoms` AS `datoms02` WHERE ((`datoms00`.a = 97 AND `datoms00`.v = $v0) OR (`datoms00`.a = 98 AND `datoms00`.v = $v1)) AND `datoms01`.a = 97 AND `datoms02`.a = 99 AND `datoms00`.e = `datoms01`.e AND `datoms00`.e = `datoms02`.e LIMIT 1"); assert_eq!(args, vec![make_arg("$v0", "http://foo.com/"), make_arg("$v1", "Foo")]); } @@ -280,7 +353,7 @@ fn test_complex_or_join() { }); } - let input = r#"[:find [?url ?description] + let query = r#"[:find [?url ?description] :where (or-join [?page] [?page :page/url "http://foo.com/"] @@ -290,7 +363,7 @@ fn test_complex_or_join() { [?save :save/title "Foo"])) [?page :page/url ?url] [?page :page/description ?description]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT `datoms04`.v AS `?url`, \ `datoms05`.v AS `?description` \ FROM (SELECT `datoms00`.e AS `?page` \ @@ -332,12 +405,12 @@ fn test_complex_or_join_type_projection() { ..Default::default() }); - let input = r#"[:find [?y] + let query = r#"[:find [?y] :where (or [6 :page/title ?y] [5 _ ?y])]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT `c00`.`?y` AS `?y`, \ `c00`.`?y_value_type_tag` AS `?y_value_type_tag` \ FROM (SELECT `datoms00`.v AS `?y`, \ @@ -359,14 +432,14 @@ fn test_with_without_aggregate() { let schema = prepopulated_schema(); // Known type. - let input = r#"[:find ?x :with ?y :where [?x :foo/bar ?y]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x :with ?y :where [?x :foo/bar ?y]]"#; + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x`, `datoms00`.v AS `?y` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99"); assert_eq!(args, vec![]); // Unknown type. - let input = r#"[:find ?x :with ?y :where [?x _ ?y]]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x :with ?y :where [?x _ ?y]]"#; + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT DISTINCT `all_datoms00`.e AS `?x`, `all_datoms00`.v AS `?y`, `all_datoms00`.value_type_tag AS `?y_value_type_tag` FROM `all_datoms` AS `all_datoms00`"); assert_eq!(args, vec![]); } @@ -376,8 +449,8 @@ fn test_order_by() { let schema = prepopulated_schema(); // Known type. - let input = r#"[:find ?x :where [?x :foo/bar ?y] :order (desc ?y)]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x :where [?x :foo/bar ?y] :order (desc ?y)]"#; + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x`, `datoms00`.v AS `?y` \ FROM `datoms` AS `datoms00` \ WHERE `datoms00`.a = 99 \ @@ -385,11 +458,11 @@ fn test_order_by() { assert_eq!(args, vec![]); // Unknown type. - let input = r#"[:find ?x :with ?y :where [?x _ ?y] :order ?y ?x]"#; - let SQLQuery { sql, args } = translate(&schema, input, None); + let query = r#"[:find ?x :with ?y :where [?x _ ?y] :order ?y ?x]"#; + let SQLQuery { sql, args } = translate(&schema, query); assert_eq!(sql, "SELECT DISTINCT `all_datoms00`.e AS `?x`, `all_datoms00`.v AS `?y`, \ `all_datoms00`.value_type_tag AS `?y_value_type_tag` \ FROM `all_datoms` AS `all_datoms00` \ ORDER BY `?y_value_type_tag` ASC, `?y` ASC, `?x` ASC"); assert_eq!(args, vec![]); -} \ No newline at end of file +} diff --git a/query/src/lib.rs b/query/src/lib.rs index 3c267b07..a0b1500d 100644 --- a/query/src/lib.rs +++ b/query/src/lib.rs @@ -370,13 +370,20 @@ pub struct Aggregate { } */ -#[derive(Clone,Debug,Eq,PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Element { Variable(Variable), // Aggregate(Aggregate), // TODO // Pull(Pull), // TODO } +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Limit { + None, + Fixed(u64), + Variable(Variable), +} + /// A definition of the first part of a find query: the /// `[:find ?foo ?bar…]` bit. /// @@ -408,7 +415,7 @@ pub enum Element { /// # } /// ``` /// -#[derive(Clone,Debug,Eq,PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum FindSpec { /// Returns an array of arrays. FindRel(Vec), @@ -610,6 +617,7 @@ pub struct FindQuery { pub with: BTreeSet, pub in_vars: BTreeSet, pub in_sources: BTreeSet, + pub limit: Limit, pub where_clauses: Vec, pub order: Option>, // TODO: in_rules; diff --git a/sql/src/lib.rs b/sql/src/lib.rs index a1c1f7b8..292ddd3d 100644 --- a/sql/src/lib.rs +++ b/sql/src/lib.rs @@ -154,7 +154,7 @@ impl QueryBuilder for SQLiteQueryBuilder { fn push_bind_param(&mut self, name: &str) -> BuildQueryResult { // Do some validation first. // This is not free, but it's probably worth it for now. - if !name.chars().all(char::is_alphanumeric) { + if !name.chars().all(|c| char::is_alphanumeric(c) || c == '_') { bail!(ErrorKind::InvalidParameterName(name.to_string())); } diff --git a/src/conn.rs b/src/conn.rs index 88a26b70..43fb7a0e 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -110,20 +110,17 @@ impl Conn { } /// Query the Mentat store, using the given connection and the current metadata. - pub fn q_once(&self, + pub fn q_once(&self, sqlite: &rusqlite::Connection, query: &str, - inputs: T, - limit: U) -> Result - where T: Into>, - U: Into> + inputs: T) -> Result + where T: Into> { q_once(sqlite, &*self.current_schema(), query, - inputs, - limit) + inputs) } /// Transact entities against the Mentat store, using the given connection and the current diff --git a/src/query.rs b/src/query.rs index 5a4f8fb0..010f4485 100644 --- a/src/query.rs +++ b/src/query.rs @@ -59,25 +59,21 @@ pub type QueryExecutionResult = Result; /// instances. /// The caller is responsible for ensuring that the SQLite connection has an open transaction if /// isolation is required. -pub fn q_once<'sqlite, 'schema, 'query, T, U> +pub fn q_once<'sqlite, 'schema, 'query, T> (sqlite: &'sqlite rusqlite::Connection, schema: &'schema Schema, query: &'query str, - inputs: T, - limit: U) -> QueryExecutionResult - where T: Into>, - U: Into> + inputs: T) -> QueryExecutionResult + where T: Into> { let parsed = parse_find_string(query)?; - let mut algebrized = algebrize_with_inputs(schema, parsed, 0, inputs.into().unwrap_or(QueryInputs::default()))?; + let algebrized = algebrize_with_inputs(schema, parsed, 0, inputs.into().unwrap_or(QueryInputs::default()))?; if algebrized.is_known_empty() { // We don't need to do any SQL work at all. return Ok(QueryResults::empty(&algebrized.find_spec)); } - algebrized.apply_limit(limit.into()); - // Because this is q_once, we can check that all of our `:in` variables are bound at this point. // If they aren't, the user has made an error -- perhaps writing the wrong variable in `:in`, or // not binding in the `QueryInput`. diff --git a/tests/query.rs b/tests/query.rs index 65c6880d..389bc94a 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -41,7 +41,7 @@ fn test_rel() { // Rel. let start = time::PreciseTime::now(); let results = q_once(&c, &db.schema, - "[:find ?x ?ident :where [?x :db/ident ?ident]]", None, None) + "[:find ?x ?ident :where [?x :db/ident ?ident]]", None) .expect("Query failed"); let end = time::PreciseTime::now(); @@ -71,7 +71,7 @@ fn test_failing_scalar() { // Scalar that fails. let start = time::PreciseTime::now(); let results = q_once(&c, &db.schema, - "[:find ?x . :where [?x :db/fulltext true]]", None, None) + "[:find ?x . :where [?x :db/fulltext true]]", None) .expect("Query failed"); let end = time::PreciseTime::now(); @@ -93,7 +93,7 @@ fn test_scalar() { // Scalar that succeeds. let start = time::PreciseTime::now(); let results = q_once(&c, &db.schema, - "[:find ?ident . :where [24 :db/ident ?ident]]", None, None) + "[:find ?ident . :where [24 :db/ident ?ident]]", None) .expect("Query failed"); let end = time::PreciseTime::now(); @@ -123,7 +123,7 @@ fn test_tuple() { "[:find [?index ?cardinality] :where [:db/txInstant :db/index ?index] [:db/txInstant :db/cardinality ?cardinality]]", - None, None) + None) .expect("Query failed"); let end = time::PreciseTime::now(); @@ -150,7 +150,7 @@ fn test_coll() { // Coll. let start = time::PreciseTime::now(); let results = q_once(&c, &db.schema, - "[:find [?e ...] :where [?e :db/ident _]]", None, None) + "[:find [?e ...] :where [?e :db/ident _]]", None) .expect("Query failed"); let end = time::PreciseTime::now(); @@ -175,7 +175,7 @@ fn test_inputs() { let ee = (Variable::from_valid_name("?e"), TypedValue::Ref(5)); let inputs = QueryInputs::with_value_sequence(vec![ee]); let results = q_once(&c, &db.schema, - "[:find ?i . :in ?e :where [?e :db/ident ?i]]", inputs, None) + "[:find ?i . :in ?e :where [?e :db/ident ?i]]", inputs) .expect("query to succeed"); if let QueryResults::Scalar(Some(TypedValue::Keyword(value))) = results { @@ -195,7 +195,7 @@ fn test_unbound_inputs() { let xx = (Variable::from_valid_name("?x"), TypedValue::Ref(5)); let inputs = QueryInputs::with_value_sequence(vec![xx]); let results = q_once(&c, &db.schema, - "[:find ?i . :in ?e :where [?e :db/ident ?i]]", inputs, None); + "[:find ?i . :in ?e :where [?e :db/ident ?i]]", inputs); match results { Result::Err(Error(ErrorKind::UnboundVariables(vars), _)) => {