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:
parent
cd860ae68d
commit
bc63744aba
15 changed files with 394 additions and 140 deletions
|
@ -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);
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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<Variable>,
|
||||
pub order: Option<Vec<OrderBy>>,
|
||||
pub limit: Option<u64>,
|
||||
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<u64>) {
|
||||
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<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,
|
||||
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<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,
|
||||
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::{
|
||||
|
|
|
@ -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<Variable>),
|
||||
In(BTreeSet<Variable>),
|
||||
WhereClauses(Vec<WhereClause>),
|
||||
Limit(Limit),
|
||||
Order(Vec<Order>),
|
||||
WhereClauses(Vec<WhereClause>),
|
||||
With(BTreeSet<Variable>),
|
||||
}
|
||||
|
||||
def_parser!(Find, vars, BTreeSet<Variable>, {
|
||||
|
@ -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<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_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<FindQueryPart>| -> std::result::Result<FindQuery, combine::primitives::Error<edn::ValueAndSpan, edn::ValueAndSpan>> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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<u64>) -> 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) => {
|
||||
|
|
|
@ -4,6 +4,8 @@ version = "0.0.1"
|
|||
workspace = ".."
|
||||
|
||||
[dependencies]
|
||||
regex = "0.2"
|
||||
|
||||
[dependencies.mentat_core]
|
||||
path = "../core"
|
||||
|
||||
|
|
|
@ -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<Constraint>,
|
||||
pub order: Vec<OrderBy>,
|
||||
pub limit: Option<u64>,
|
||||
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 {
|
||||
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();
|
||||
|
|
|
@ -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<T>(projection: Projection,
|
||||
fn cc_to_select_query(projection: Projection,
|
||||
cc: ConjoiningClauses,
|
||||
distinct: bool,
|
||||
order: Option<Vec<OrderBy>>,
|
||||
limit: T) -> SelectQuery where T: Into<Option<u64>> {
|
||||
limit: Limit) -> SelectQuery {
|
||||
let from = if cc.from.is_empty() {
|
||||
FromClause::Nothing
|
||||
} else {
|
||||
|
@ -268,7 +270,7 @@ fn cc_to_select_query<T>(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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<T: Into<Option<u64>>>(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<String>) {
|
|||
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,8 +458,8 @@ 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` \
|
||||
|
|
|
@ -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<Element>),
|
||||
|
@ -610,6 +617,7 @@ pub struct FindQuery {
|
|||
pub with: BTreeSet<Variable>,
|
||||
pub in_vars: BTreeSet<Variable>,
|
||||
pub in_sources: BTreeSet<SrcVar>,
|
||||
pub limit: Limit,
|
||||
pub where_clauses: Vec<WhereClause>,
|
||||
pub order: Option<Vec<Order>>,
|
||||
// TODO: in_rules;
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
|
|
11
src/conn.rs
11
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<T, U>(&self,
|
||||
pub fn q_once<T>(&self,
|
||||
sqlite: &rusqlite::Connection,
|
||||
query: &str,
|
||||
inputs: T,
|
||||
limit: U) -> Result<QueryResults>
|
||||
where T: Into<Option<QueryInputs>>,
|
||||
U: Into<Option<u64>>
|
||||
inputs: T) -> Result<QueryResults>
|
||||
where T: Into<Option<QueryInputs>>
|
||||
{
|
||||
|
||||
q_once(sqlite,
|
||||
&*self.current_schema(),
|
||||
query,
|
||||
inputs,
|
||||
limit)
|
||||
inputs)
|
||||
}
|
||||
|
||||
/// Transact entities against the Mentat store, using the given connection and the current
|
||||
|
|
12
src/query.rs
12
src/query.rs
|
@ -59,25 +59,21 @@ pub type QueryExecutionResult = Result<QueryResults>;
|
|||
/// 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<Option<QueryInputs>>,
|
||||
U: Into<Option<u64>>
|
||||
inputs: T) -> QueryExecutionResult
|
||||
where T: Into<Option<QueryInputs>>
|
||||
{
|
||||
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`.
|
||||
|
|
|
@ -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), _)) => {
|
||||
|
|
Loading…
Reference in a new issue