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.
This commit is contained in:
Richard Newman 2017-04-19 16:16:19 -07:00 committed by GitHub
parent cd860ae68d
commit bc63744aba
15 changed files with 394 additions and 140 deletions

View file

@ -441,6 +441,11 @@ impl ConjoiningClauses {
QueryValue::PrimitiveLong(value))) 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. /// Mark the given value as one of the set of numeric types.
fn constrain_var_to_numeric(&mut self, variable: Variable) { fn constrain_var_to_numeric(&mut self, variable: Variable) {
let mut numeric_types = HashSet::with_capacity(2); let mut numeric_types = HashSet::with_capacity(2);

View file

@ -47,6 +47,11 @@ error_chain! {
display("invalid argument to {}: expected numeric in position {}.", function, position) 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 { NonMatchingVariablesInOrClause {
// TODO: flesh out. // TODO: flesh out.
description("non-matching variables in 'or' clause") description("non-matching variables in 'or' clause")

View file

@ -24,6 +24,8 @@ mod clauses;
use mentat_core::{ use mentat_core::{
Schema, Schema,
TypedValue,
ValueType,
}; };
use mentat_core::counter::RcCounter; use mentat_core::counter::RcCounter;
@ -31,6 +33,7 @@ use mentat_core::counter::RcCounter;
use mentat_query::{ use mentat_query::{
FindQuery, FindQuery,
FindSpec, FindSpec,
Limit,
Order, Order,
SrcVar, SrcVar,
Variable, Variable,
@ -53,28 +56,11 @@ pub struct AlgebraicQuery {
has_aggregates: bool, has_aggregates: bool,
pub with: BTreeSet<Variable>, pub with: BTreeSet<Variable>,
pub order: Option<Vec<OrderBy>>, pub order: Option<Vec<OrderBy>>,
pub limit: Option<u64>, pub limit: Limit,
pub cc: clauses::ConjoiningClauses, pub cc: clauses::ConjoiningClauses,
} }
impl AlgebraicQuery { 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<u64>) {
match self.limit {
None => self.limit = limit,
Some(existing) =>
match limit {
None => (),
Some(new) =>
if new < existing {
self.limit = limit;
},
},
};
}
#[inline] #[inline]
pub fn is_known_empty(&self) -> bool { pub fn is_known_empty(&self) -> bool {
self.cc.is_known_empty() self.cc.is_known_empty()
@ -131,6 +117,45 @@ fn validate_and_simplify_order(cc: &ConjoiningClauses, order: Option<Vec<Order>>
} }
} }
fn simplify_limit(mut query: AlgebraicQuery) -> Result<AlgebraicQuery> {
// 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, pub fn algebrize_with_inputs(schema: &Schema,
parsed: FindQuery, parsed: FindQuery,
counter: usize, counter: usize,
@ -138,6 +163,11 @@ pub fn algebrize_with_inputs(schema: &Schema,
let alias_counter = RcCounter::with_initial(counter); let alias_counter = RcCounter::with_initial(counter);
let mut cc = ConjoiningClauses::with_inputs_and_alias_counter(parsed.in_vars, inputs, alias_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: integrate default source into pattern processing.
// TODO: flesh out the rest of find-into-context. // TODO: flesh out the rest of find-into-context.
let where_clauses = parsed.where_clauses; 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 (order, extra_vars) = validate_and_simplify_order(&cc, parsed.order)?;
let with: BTreeSet<Variable> = parsed.with.into_iter().chain(extra_vars.into_iter()).collect(); let with: BTreeSet<Variable> = 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, default_source: parsed.default_source,
find_spec: parsed.find_spec, find_spec: parsed.find_spec,
has_aggregates: false, // TODO: we don't parse them yet. has_aggregates: false, // TODO: we don't parse them yet.
@ -158,7 +190,10 @@ pub fn algebrize_with_inputs(schema: &Schema,
order: order, order: order,
limit: limit, limit: limit,
cc: cc, cc: cc,
}) };
// Substitute in any fixed values and fail if they're out of range.
simplify_limit(q)
} }
pub use clauses::{ pub use clauses::{

View file

@ -18,7 +18,7 @@ use std; // To refer to std::result::Result.
use std::collections::BTreeSet; use std::collections::BTreeSet;
use self::combine::{eof, many, many1, optional, parser, satisfy, satisfy_map, Parser, ParseResult, Stream}; 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::{ use self::mentat_parser_utils::{
ResultParser, ResultParser,
@ -43,6 +43,7 @@ use self::mentat_query::{
FindSpec, FindSpec,
FnArg, FnArg,
FromValue, FromValue,
Limit,
Order, Order,
OrJoin, OrJoin,
OrWhereClause, OrWhereClause,
@ -102,6 +103,16 @@ error_chain! {
description("missing field") description("missing field")
display("missing field: '{}'", value) 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) 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, { def_parser!(Where, pattern_non_value_place, PatternNonValuePlace, {
satisfy_map(PatternNonValuePlace::from_value) 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_find, "find");
def_matches_keyword!(Find, literal_in, "in"); def_matches_keyword!(Find, literal_in, "in");
def_matches_keyword!(Find, literal_limit, "limit");
def_matches_keyword!(Find, literal_with, "with");
def_matches_keyword!(Find, literal_where, "where");
def_matches_keyword!(Find, literal_order, "order"); 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`. /// Express something close to a builder pattern for a `FindQuery`.
enum FindQueryPart { enum FindQueryPart {
FindSpec(FindSpec), FindSpec(FindSpec),
With(BTreeSet<Variable>),
In(BTreeSet<Variable>), In(BTreeSet<Variable>),
WhereClauses(Vec<WhereClause>), Limit(Limit),
Order(Vec<Order>), Order(Vec<Order>),
WhereClauses(Vec<WhereClause>),
With(BTreeSet<Variable>),
} }
def_parser!(Find, vars, BTreeSet<Variable>, { def_parser!(Find, vars, BTreeSet<Variable>, {
@ -373,49 +396,72 @@ def_parser!(Find, query, FindQuery, {
let p_find_spec = Find::literal_find() let p_find_spec = Find::literal_find()
.with(vector().of_exactly(Find::spec().map(FindQueryPart::FindSpec))); .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_limit = Find::literal_limit()
.with(vector().of_exactly(
let p_where_clauses = Find::literal_where() Query::variable().map(|v| Limit::Variable(v))
.with(vector().of_exactly(Where::clauses().map(FindQueryPart::WhereClauses))).expected(":where clauses"); .or(Query::natural_number().map(|n| Limit::Fixed(n)))))
.map(FindQueryPart::Limit);
let p_order_clauses = Find::literal_order() let p_order_clauses = Find::literal_order()
.with(vector().of_exactly(many1(Query::order()).map(FindQueryPart::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())) (or(map(), keyword_map()))
.of_exactly(many(choice::<[&mut Parser<Input = ValueStream, Output = FindQueryPart>; 5], _>([ .of_exactly(many(choice::<[&mut Parser<Input = ValueStream, Output = FindQueryPart>; 6], _>([
// Ordered by likelihood.
&mut try(p_find_spec), &mut try(p_find_spec),
&mut try(p_in_vars),
&mut try(p_with_vars),
&mut try(p_where_clauses), &mut try(p_where_clauses),
&mut try(p_in_vars),
&mut try(p_limit),
&mut try(p_order_clauses), &mut try(p_order_clauses),
&mut try(p_with_vars),
]))) ])))
.and_then(|parts: Vec<FindQueryPart>| -> std::result::Result<FindQuery, combine::primitives::Error<edn::ValueAndSpan, edn::ValueAndSpan>> { .and_then(|parts: Vec<FindQueryPart>| -> std::result::Result<FindQuery, combine::primitives::Error<edn::ValueAndSpan, edn::ValueAndSpan>> {
let mut find_spec = None; let mut find_spec = None;
let mut in_vars = None; let mut in_vars = None;
let mut with_vars = None; let mut limit = Limit::None;
let mut where_clauses = None;
let mut order_clauses = None; let mut order_clauses = None;
let mut where_clauses = None;
let mut with_vars = None;
for part in parts { for part in parts {
match part { match part {
FindQueryPart::FindSpec(x) => find_spec = Some(x), FindQueryPart::FindSpec(x) => find_spec = Some(x),
FindQueryPart::With(x) => with_vars = Some(x),
FindQueryPart::In(x) => in_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::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 { Ok(FindQuery {
find_spec: find_spec.clone().ok_or(combine::primitives::Error::Unexpected("expected :find".into()))?,
default_source: SrcVar::DefaultSrc, default_source: SrcVar::DefaultSrc,
with: with_vars.unwrap_or(BTreeSet::default()), find_spec: find_spec.clone().ok_or(combine::primitives::Error::Unexpected("expected :find".into()))?,
in_vars: in_vars.unwrap_or(BTreeSet::default()),
in_sources: BTreeSet::default(), // TODO in_sources: BTreeSet::default(), // TODO
in_vars: in_vars,
limit: limit,
order: order_clauses, order: order_clauses,
where_clauses: where_clauses.ok_or(combine::primitives::Error::Unexpected("expected :where".into()))?, 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)), FindSpec::FindTuple(vec![Element::Variable(variable(vx)),
Element::Variable(variable(vy))])); 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());
}
} }

View file

@ -8,9 +8,9 @@
// CONDITIONS OF ANY KIND, either express or implied. See the License for the // CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License. // specific language governing permissions and limitations under the License.
extern crate mentat_query_parser;
extern crate mentat_query;
extern crate edn; extern crate edn;
extern crate mentat_query;
extern crate mentat_query_parser;
use edn::{ use edn::{
NamespacedKeyword, NamespacedKeyword,
@ -22,6 +22,7 @@ use mentat_query::{
Element, Element,
FindSpec, FindSpec,
FnArg, FnArg,
Limit,
Order, Order,
OrJoin, OrJoin,
OrWhereClause, OrWhereClause,
@ -123,7 +124,7 @@ fn can_parse_unit_or_join() {
#[test] #[test]
fn can_parse_simple_or_join() { 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(); let p = parse_find_string(s).unwrap();
assert_eq!(p.find_spec, assert_eq!(p.find_spec,
@ -146,7 +147,7 @@ fn can_parse_simple_or_join() {
source: None, source: None,
entity: PatternNonValuePlace::Variable(Variable::from_valid_name("?x")), entity: PatternNonValuePlace::Variable(Variable::from_valid_name("?x")),
attribute: PatternNonValuePlace::Placeholder, attribute: PatternNonValuePlace::Placeholder,
value: PatternValuePlace::EntidOrInteger(15), value: PatternValuePlace::EntidOrInteger(-15),
tx: PatternNonValuePlace::Placeholder, tx: PatternNonValuePlace::Placeholder,
})), })),
], ],
@ -234,3 +235,35 @@ fn can_parse_order_by() {
Some(vec![Order(Direction::Descending, Variable::from_valid_name("?y")), Some(vec![Order(Direction::Descending, Variable::from_valid_name("?y")),
Order(Direction::Ascending, Variable::from_valid_name("?x"))])); 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());
}

View file

@ -37,6 +37,7 @@ use mentat_db::{
use mentat_query::{ use mentat_query::{
Element, Element,
FindSpec, FindSpec,
Limit,
Variable, Variable,
}; };
@ -442,8 +443,8 @@ pub struct CombinedProjection {
} }
impl CombinedProjection { impl CombinedProjection {
fn flip_distinct_for_limit(mut self, limit: Option<u64>) -> Self { fn flip_distinct_for_limit(mut self, limit: &Limit) -> Self {
if limit == Some(1) { if *limit == Limit::Fixed(1) {
self.distinct = false; self.distinct = false;
} }
self self
@ -474,7 +475,7 @@ pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection {
match query.find_spec { match query.find_spec {
FindColl(ref element) => { FindColl(ref element) => {
let (cols, templates) = project_elements(1, iter::once(element), query); 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) => { FindScalar(ref element) => {
@ -485,7 +486,7 @@ pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection {
FindRel(ref elements) => { FindRel(ref elements) => {
let column_count = query.find_spec.expected_column_count(); let column_count = query.find_spec.expected_column_count();
let (cols, templates) = project_elements(column_count, elements, query); 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) => { FindTuple(ref elements) => {

View file

@ -4,6 +4,8 @@ version = "0.0.1"
workspace = ".." workspace = ".."
[dependencies] [dependencies]
regex = "0.2"
[dependencies.mentat_core] [dependencies.mentat_core]
path = "../core" path = "../core"

View file

@ -8,6 +8,7 @@
// CONDITIONS OF ANY KIND, either express or implied. See the License for the // CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License. // specific language governing permissions and limitations under the License.
extern crate regex;
extern crate mentat_core; extern crate mentat_core;
extern crate mentat_query; extern crate mentat_query;
extern crate mentat_query_algebrizer; extern crate mentat_query_algebrizer;
@ -20,6 +21,8 @@ use mentat_core::{
use mentat_query::{ use mentat_query::{
Direction, Direction,
Limit,
Variable,
}; };
use mentat_query_algebrizer::{ use mentat_query_algebrizer::{
@ -158,7 +161,7 @@ pub struct SelectQuery {
pub from: FromClause, pub from: FromClause,
pub constraints: Vec<Constraint>, pub constraints: Vec<Constraint>,
pub order: Vec<OrderBy>, pub order: Vec<OrderBy>,
pub limit: Option<u64>, pub limit: Limit,
} }
fn push_variable_column(qb: &mut QueryBuilder, vc: &VariableColumn) -> BuildQueryResult { 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 { impl QueryFragment for SelectQuery {
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult { fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
if self.distinct { if self.distinct {
@ -430,10 +446,18 @@ impl QueryFragment for SelectQuery {
{ out.push_sql(", ") }); { out.push_sql(", ") });
} }
// Guaranteed to be positive: u64. match &self.limit {
if let Some(limit) = self.limit { &Limit::None => (),
&Limit::Fixed(limit) => {
// Guaranteed to be non-negative: u64.
out.push_sql(" LIMIT "); out.push_sql(" LIMIT ");
out.push_sql(limit.to_string().as_str()); 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(()) Ok(())
@ -554,7 +578,7 @@ mod tests {
}, },
], ],
order: vec![], order: vec![],
limit: None, limit: Limit::None,
}; };
let SQLQuery { sql, args } = query.to_sql_query().unwrap(); let SQLQuery { sql, args } = query.to_sql_query().unwrap();

View file

@ -14,6 +14,8 @@ use mentat_core::{
ValueType, ValueType,
}; };
use mentat_query::Limit;
use mentat_query_algebrizer::{ use mentat_query_algebrizer::{
AlgebraicQuery, AlgebraicQuery,
ColumnAlternation, ColumnAlternation,
@ -224,7 +226,7 @@ fn table_for_computed(computed: ComputedTable, alias: TableAlias) -> TableOrSubq
// Each arm simply turns into a subquery. // Each arm simply turns into a subquery.
// The SQL translation will stuff "UNION" between each arm. // The SQL translation will stuff "UNION" between each arm.
let projection = Projection::Columns(columns); 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(), }).collect(),
alias) 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 /// 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 /// query that runs SQL. The next level up the call stack can check for known-empty queries if
/// needed. /// needed.
fn cc_to_select_query<T>(projection: Projection, fn cc_to_select_query(projection: Projection,
cc: ConjoiningClauses, cc: ConjoiningClauses,
distinct: bool, distinct: bool,
order: Option<Vec<OrderBy>>, order: Option<Vec<OrderBy>>,
limit: T) -> SelectQuery where T: Into<Option<u64>> { limit: Limit) -> SelectQuery {
let from = if cc.from.is_empty() { let from = if cc.from.is_empty() {
FromClause::Nothing FromClause::Nothing
} else { } else {
@ -268,7 +270,7 @@ fn cc_to_select_query<T>(projection: Projection,
// Turn the query-centric order clauses into column-orders. // 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 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 { SelectQuery {
distinct: distinct, distinct: distinct,
projection: projection, projection: projection,
@ -293,10 +295,10 @@ pub fn cc_to_exists(cc: ConjoiningClauses) -> SelectQuery {
from: FromClause::Nothing, from: FromClause::Nothing,
constraints: vec![], constraints: vec![],
order: vec![], order: vec![],
limit: Some(0), limit: Limit::Fixed(0),
} }
} else { } else {
cc_to_select_query(Projection::One, cc, false, None, 1) cc_to_select_query(Projection::One, cc, false, None, Limit::Fixed(1))
} }
} }

View file

@ -17,17 +17,25 @@ extern crate mentat_sql;
use std::rc::Rc; use std::rc::Rc;
use mentat_query::NamespacedKeyword; use mentat_query::{
NamespacedKeyword,
Variable,
};
use mentat_core::{ use mentat_core::{
Attribute, Attribute,
Entid, Entid,
Schema, Schema,
TypedValue,
ValueType, ValueType,
}; };
use mentat_query_parser::parse_find_string; 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::{ use mentat_query_translator::{
query_to_select, query_to_select,
}; };
@ -43,14 +51,17 @@ fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) {
schema.schema_map.insert(e, a); schema.schema_map.insert(e, a);
} }
fn translate<T: Into<Option<u64>>>(schema: &Schema, input: &'static str, limit: T) -> SQLQuery { fn translate_with_inputs(schema: &Schema, query: &'static str, inputs: QueryInputs) -> SQLQuery {
let parsed = parse_find_string(input).expect("parse failed"); let parsed = parse_find_string(query).expect("parse failed");
let mut algebrized = algebrize(schema, parsed).expect("algebrize failed"); let algebrized = algebrize_with_inputs(schema, parsed, 0, inputs).expect("algebrize failed");
algebrized.apply_limit(limit.into());
let select = query_to_select(algebrized); let select = query_to_select(algebrized);
select.query.to_sql_query().unwrap() 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 { fn prepopulated_typed_schema(foo_type: ValueType) -> Schema {
let mut schema = Schema::default(); let mut schema = Schema::default();
associate_ident(&mut schema, NamespacedKeyword::new("foo", "bar"), 99); associate_ident(&mut schema, NamespacedKeyword::new("foo", "bar"), 99);
@ -73,8 +84,8 @@ fn make_arg(name: &'static str, value: &'static str) -> (String, Rc<String>) {
fn test_scalar() { fn test_scalar() {
let schema = prepopulated_schema(); let schema = prepopulated_schema();
let input = r#"[:find ?x . :where [?x :foo/bar "yyy"]]"#; let query = r#"[:find ?x . :where [?x :foo/bar "yyy"]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); 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!(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")]); assert_eq!(args, vec![make_arg("$v0", "yyy")]);
} }
@ -83,8 +94,8 @@ fn test_scalar() {
fn test_tuple() { fn test_tuple() {
let schema = prepopulated_schema(); let schema = prepopulated_schema();
let input = r#"[:find [?x] :where [?x :foo/bar "yyy"]]"#; let query = r#"[:find [?x] :where [?x :foo/bar "yyy"]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); 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!(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")]); assert_eq!(args, vec![make_arg("$v0", "yyy")]);
} }
@ -93,8 +104,8 @@ fn test_tuple() {
fn test_coll() { fn test_coll() {
let schema = prepopulated_schema(); let schema = prepopulated_schema();
let input = r#"[:find [?x ...] :where [?x :foo/bar "yyy"]]"#; let query = r#"[:find [?x ...] :where [?x :foo/bar "yyy"]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); 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!(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")]); assert_eq!(args, vec![make_arg("$v0", "yyy")]);
} }
@ -103,8 +114,8 @@ fn test_coll() {
fn test_rel() { fn test_rel() {
let schema = prepopulated_schema(); let schema = prepopulated_schema();
let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#; let query = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); 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!(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")]); assert_eq!(args, vec![make_arg("$v0", "yyy")]);
} }
@ -113,18 +124,80 @@ fn test_rel() {
fn test_limit() { fn test_limit() {
let schema = prepopulated_schema(); let schema = prepopulated_schema();
let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#; let query = r#"[:find ?x :where [?x :foo/bar "yyy"] :limit 5]"#;
let SQLQuery { sql, args } = translate(&schema, input, 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!(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")]); 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] #[test]
fn test_unknown_attribute_keyword_value() { fn test_unknown_attribute_keyword_value() {
let schema = Schema::default(); let schema = Schema::default();
let input = r#"[:find ?x :where [?x _ :ab/yyy]]"#; let query = r#"[:find ?x :where [?x _ :ab/yyy]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); let SQLQuery { sql, args } = translate(&schema, query);
// Only match keywords, not strings: tag = 13. // 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"); 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() { fn test_unknown_attribute_string_value() {
let schema = Schema::default(); let schema = Schema::default();
let input = r#"[:find ?x :where [?x _ "horses"]]"#; let query = r#"[:find ?x :where [?x _ "horses"]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); let SQLQuery { sql, args } = translate(&schema, query);
// We expect all_datoms because we're querying for a string. Magic, that. // We expect all_datoms because we're querying for a string. Magic, that.
// We don't want keywords etc., so tag = 10. // 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() { fn test_unknown_attribute_double_value() {
let schema = Schema::default(); let schema = Schema::default();
let input = r#"[:find ?x :where [?x _ 9.95]]"#; let query = r#"[:find ?x :where [?x _ 9.95]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); 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 // 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. // make sure we only match numbers.
@ -167,22 +240,22 @@ fn test_unknown_attribute_integer_value() {
let two = r#"[:find ?x :where [?x _ 2]]"#; let two = r#"[:find ?x :where [?x _ 2]]"#;
// Can't match boolean; no need to filter it out. // 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!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = -1");
assert_eq!(args, vec![]); assert_eq!(args, vec![]);
// Excludes booleans. // 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!(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![]); assert_eq!(args, vec![]);
// Excludes booleans. // 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!(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![]); assert_eq!(args, vec![]);
// Can't match boolean; no need to filter it out. // 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!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.v = 2");
assert_eq!(args, vec![]); assert_eq!(args, vec![]);
} }
@ -208,8 +281,8 @@ fn test_unknown_ident() {
fn test_numeric_less_than_unknown_attribute() { fn test_numeric_less_than_unknown_attribute() {
let schema = Schema::default(); let schema = Schema::default();
let input = r#"[:find ?x :where [?x _ ?y] [(< ?y 10)]]"#; let query = r#"[:find ?x :where [?x _ ?y] [(< ?y 10)]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); let SQLQuery { sql, args } = translate(&schema, query);
// Although we infer numericness from numeric predicates, we've already assigned a table to the // Although we infer numericness from numeric predicates, we've already assigned a table to the
// first pattern, and so this is _still_ `all_datoms`. // first pattern, and so this is _still_ `all_datoms`.
@ -220,8 +293,8 @@ fn test_numeric_less_than_unknown_attribute() {
#[test] #[test]
fn test_numeric_gte_known_attribute() { fn test_numeric_gte_known_attribute() {
let schema = prepopulated_typed_schema(ValueType::Double); let schema = prepopulated_typed_schema(ValueType::Double);
let input = r#"[:find ?x :where [?x :foo/bar ?y] [(>= ?y 12.9)]]"#; let query = r#"[:find ?x :where [?x :foo/bar ?y] [(>= ?y 12.9)]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); 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!(sql, "SELECT DISTINCT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v >= 12.9");
assert_eq!(args, vec![]); assert_eq!(args, vec![]);
} }
@ -229,8 +302,8 @@ fn test_numeric_gte_known_attribute() {
#[test] #[test]
fn test_numeric_not_equals_known_attribute() { fn test_numeric_not_equals_known_attribute() {
let schema = prepopulated_typed_schema(ValueType::Long); let schema = prepopulated_typed_schema(ValueType::Long);
let input = r#"[:find ?x . :where [?x :foo/bar ?y] [(!= ?y 12)]]"#; let query = r#"[:find ?x . :where [?x :foo/bar ?y] [(!= ?y 12)]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); 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!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v <> 12 LIMIT 1");
assert_eq!(args, vec![]); 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 :where
(or-join [?page] (or-join [?page]
[?page :page/url "http://foo.com/"] [?page :page/url "http://foo.com/"]
[?page :page/title "Foo"]) [?page :page/title "Foo"])
[?page :page/url ?url] [?page :page/url ?url]
[?page :page/description ?description]]"#; [?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!(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")]); 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 :where
(or-join [?page] (or-join [?page]
[?page :page/url "http://foo.com/"] [?page :page/url "http://foo.com/"]
@ -290,7 +363,7 @@ fn test_complex_or_join() {
[?save :save/title "Foo"])) [?save :save/title "Foo"]))
[?page :page/url ?url] [?page :page/url ?url]
[?page :page/description ?description]]"#; [?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`, \ assert_eq!(sql, "SELECT `datoms04`.v AS `?url`, \
`datoms05`.v AS `?description` \ `datoms05`.v AS `?description` \
FROM (SELECT `datoms00`.e AS `?page` \ FROM (SELECT `datoms00`.e AS `?page` \
@ -332,12 +405,12 @@ fn test_complex_or_join_type_projection() {
..Default::default() ..Default::default()
}); });
let input = r#"[:find [?y] let query = r#"[:find [?y]
:where :where
(or (or
[6 :page/title ?y] [6 :page/title ?y]
[5 _ ?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`, \ assert_eq!(sql, "SELECT `c00`.`?y` AS `?y`, \
`c00`.`?y_value_type_tag` AS `?y_value_type_tag` \ `c00`.`?y_value_type_tag` AS `?y_value_type_tag` \
FROM (SELECT `datoms00`.v AS `?y`, \ FROM (SELECT `datoms00`.v AS `?y`, \
@ -359,14 +432,14 @@ fn test_with_without_aggregate() {
let schema = prepopulated_schema(); let schema = prepopulated_schema();
// Known type. // Known type.
let input = r#"[:find ?x :with ?y :where [?x :foo/bar ?y]]"#; let query = r#"[:find ?x :with ?y :where [?x :foo/bar ?y]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); 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!(sql, "SELECT DISTINCT `datoms00`.e AS `?x`, `datoms00`.v AS `?y` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99");
assert_eq!(args, vec![]); assert_eq!(args, vec![]);
// Unknown type. // Unknown type.
let input = r#"[:find ?x :with ?y :where [?x _ ?y]]"#; let query = r#"[:find ?x :with ?y :where [?x _ ?y]]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); 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!(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![]); assert_eq!(args, vec![]);
} }
@ -376,8 +449,8 @@ fn test_order_by() {
let schema = prepopulated_schema(); let schema = prepopulated_schema();
// Known type. // Known type.
let input = r#"[:find ?x :where [?x :foo/bar ?y] :order (desc ?y)]"#; let query = r#"[:find ?x :where [?x :foo/bar ?y] :order (desc ?y)]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); let SQLQuery { sql, args } = translate(&schema, query);
assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x`, `datoms00`.v AS `?y` \ assert_eq!(sql, "SELECT DISTINCT `datoms00`.e AS `?x`, `datoms00`.v AS `?y` \
FROM `datoms` AS `datoms00` \ FROM `datoms` AS `datoms00` \
WHERE `datoms00`.a = 99 \ WHERE `datoms00`.a = 99 \
@ -385,8 +458,8 @@ fn test_order_by() {
assert_eq!(args, vec![]); assert_eq!(args, vec![]);
// Unknown type. // Unknown type.
let input = r#"[:find ?x :with ?y :where [?x _ ?y] :order ?y ?x]"#; let query = r#"[:find ?x :with ?y :where [?x _ ?y] :order ?y ?x]"#;
let SQLQuery { sql, args } = translate(&schema, input, None); let SQLQuery { sql, args } = translate(&schema, query);
assert_eq!(sql, "SELECT DISTINCT `all_datoms00`.e AS `?x`, `all_datoms00`.v AS `?y`, \ 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` \ `all_datoms00`.value_type_tag AS `?y_value_type_tag` \
FROM `all_datoms` AS `all_datoms00` \ FROM `all_datoms` AS `all_datoms00` \

View file

@ -377,6 +377,13 @@ pub enum Element {
// Pull(Pull), // 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 /// A definition of the first part of a find query: the
/// `[:find ?foo ?bar…]` bit. /// `[:find ?foo ?bar…]` bit.
/// ///
@ -610,6 +617,7 @@ pub struct FindQuery {
pub with: BTreeSet<Variable>, pub with: BTreeSet<Variable>,
pub in_vars: BTreeSet<Variable>, pub in_vars: BTreeSet<Variable>,
pub in_sources: BTreeSet<SrcVar>, pub in_sources: BTreeSet<SrcVar>,
pub limit: Limit,
pub where_clauses: Vec<WhereClause>, pub where_clauses: Vec<WhereClause>,
pub order: Option<Vec<Order>>, pub order: Option<Vec<Order>>,
// TODO: in_rules; // TODO: in_rules;

View file

@ -154,7 +154,7 @@ impl QueryBuilder for SQLiteQueryBuilder {
fn push_bind_param(&mut self, name: &str) -> BuildQueryResult { fn push_bind_param(&mut self, name: &str) -> BuildQueryResult {
// Do some validation first. // Do some validation first.
// This is not free, but it's probably worth it for now. // 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())); bail!(ErrorKind::InvalidParameterName(name.to_string()));
} }

View file

@ -110,20 +110,17 @@ impl Conn {
} }
/// Query the Mentat store, using the given connection and the current metadata. /// Query the Mentat store, using the given connection and the current metadata.
pub fn q_once<T, U>(&self, pub fn q_once<T>(&self,
sqlite: &rusqlite::Connection, sqlite: &rusqlite::Connection,
query: &str, query: &str,
inputs: T, inputs: T) -> Result<QueryResults>
limit: U) -> Result<QueryResults> where T: Into<Option<QueryInputs>>
where T: Into<Option<QueryInputs>>,
U: Into<Option<u64>>
{ {
q_once(sqlite, q_once(sqlite,
&*self.current_schema(), &*self.current_schema(),
query, query,
inputs, inputs)
limit)
} }
/// Transact entities against the Mentat store, using the given connection and the current /// Transact entities against the Mentat store, using the given connection and the current

View file

@ -59,25 +59,21 @@ pub type QueryExecutionResult = Result<QueryResults>;
/// instances. /// instances.
/// The caller is responsible for ensuring that the SQLite connection has an open transaction if /// The caller is responsible for ensuring that the SQLite connection has an open transaction if
/// isolation is required. /// isolation is required.
pub fn q_once<'sqlite, 'schema, 'query, T, U> pub fn q_once<'sqlite, 'schema, 'query, T>
(sqlite: &'sqlite rusqlite::Connection, (sqlite: &'sqlite rusqlite::Connection,
schema: &'schema Schema, schema: &'schema Schema,
query: &'query str, query: &'query str,
inputs: T, inputs: T) -> QueryExecutionResult
limit: U) -> QueryExecutionResult where T: Into<Option<QueryInputs>>
where T: Into<Option<QueryInputs>>,
U: Into<Option<u64>>
{ {
let parsed = parse_find_string(query)?; 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() { if algebrized.is_known_empty() {
// We don't need to do any SQL work at all. // We don't need to do any SQL work at all.
return Ok(QueryResults::empty(&algebrized.find_spec)); 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. // 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 // If they aren't, the user has made an error -- perhaps writing the wrong variable in `:in`, or
// not binding in the `QueryInput`. // not binding in the `QueryInput`.

View file

@ -41,7 +41,7 @@ fn test_rel() {
// Rel. // Rel.
let start = time::PreciseTime::now(); let start = time::PreciseTime::now();
let results = q_once(&c, &db.schema, 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"); .expect("Query failed");
let end = time::PreciseTime::now(); let end = time::PreciseTime::now();
@ -71,7 +71,7 @@ fn test_failing_scalar() {
// Scalar that fails. // Scalar that fails.
let start = time::PreciseTime::now(); let start = time::PreciseTime::now();
let results = q_once(&c, &db.schema, 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"); .expect("Query failed");
let end = time::PreciseTime::now(); let end = time::PreciseTime::now();
@ -93,7 +93,7 @@ fn test_scalar() {
// Scalar that succeeds. // Scalar that succeeds.
let start = time::PreciseTime::now(); let start = time::PreciseTime::now();
let results = q_once(&c, &db.schema, 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"); .expect("Query failed");
let end = time::PreciseTime::now(); let end = time::PreciseTime::now();
@ -123,7 +123,7 @@ fn test_tuple() {
"[:find [?index ?cardinality] "[:find [?index ?cardinality]
:where [:db/txInstant :db/index ?index] :where [:db/txInstant :db/index ?index]
[:db/txInstant :db/cardinality ?cardinality]]", [:db/txInstant :db/cardinality ?cardinality]]",
None, None) None)
.expect("Query failed"); .expect("Query failed");
let end = time::PreciseTime::now(); let end = time::PreciseTime::now();
@ -150,7 +150,7 @@ fn test_coll() {
// Coll. // Coll.
let start = time::PreciseTime::now(); let start = time::PreciseTime::now();
let results = q_once(&c, &db.schema, 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"); .expect("Query failed");
let end = time::PreciseTime::now(); let end = time::PreciseTime::now();
@ -175,7 +175,7 @@ fn test_inputs() {
let ee = (Variable::from_valid_name("?e"), TypedValue::Ref(5)); let ee = (Variable::from_valid_name("?e"), TypedValue::Ref(5));
let inputs = QueryInputs::with_value_sequence(vec![ee]); let inputs = QueryInputs::with_value_sequence(vec![ee]);
let results = q_once(&c, &db.schema, 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"); .expect("query to succeed");
if let QueryResults::Scalar(Some(TypedValue::Keyword(value))) = results { 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 xx = (Variable::from_valid_name("?x"), TypedValue::Ref(5));
let inputs = QueryInputs::with_value_sequence(vec![xx]); let inputs = QueryInputs::with_value_sequence(vec![xx]);
let results = q_once(&c, &db.schema, 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 { match results {
Result::Err(Error(ErrorKind::UnboundVariables(vars), _)) => { Result::Err(Error(ErrorKind::UnboundVariables(vars), _)) => {