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)))
}
/// 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);

View file

@ -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")

View file

@ -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::{

View file

@ -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());
}
}

View file

@ -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());
}

View file

@ -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) => {

View file

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

View file

@ -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 {
out.push_sql(" LIMIT ");
out.push_sql(limit.to_string().as_str());
match &self.limit {
&Limit::None => (),
&Limit::Fixed(limit) => {
// Guaranteed to be non-negative: u64.
out.push_sql(" LIMIT ");
out.push_sql(limit.to_string().as_str());
},
&Limit::Variable(ref var) => {
// Guess this wasn't bound yet. Produce an argument.
out.push_sql(" LIMIT ");
self.push_variable_param(var, out)?;
},
}
Ok(())
@ -554,7 +578,7 @@ mod tests {
},
],
order: vec![],
limit: None,
limit: Limit::None,
};
let SQLQuery { sql, args } = query.to_sql_query().unwrap();

View file

@ -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,
cc: ConjoiningClauses,
distinct: bool,
order: Option<Vec<OrderBy>>,
limit: T) -> SelectQuery where T: Into<Option<u64>> {
fn cc_to_select_query(projection: Projection,
cc: ConjoiningClauses,
distinct: bool,
order: Option<Vec<OrderBy>>,
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))
}
}

View file

@ -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,11 +458,11 @@ fn test_order_by() {
assert_eq!(args, vec![]);
// Unknown type.
let input = r#"[:find ?x :with ?y :where [?x _ ?y] :order ?y ?x]"#;
let SQLQuery { sql, args } = translate(&schema, input, None);
let query = r#"[:find ?x :with ?y :where [?x _ ?y] :order ?y ?x]"#;
let SQLQuery { sql, args } = translate(&schema, query);
assert_eq!(sql, "SELECT DISTINCT `all_datoms00`.e AS `?x`, `all_datoms00`.v AS `?y`, \
`all_datoms00`.value_type_tag AS `?y_value_type_tag` \
FROM `all_datoms` AS `all_datoms00` \
ORDER BY `?y_value_type_tag` ASC, `?y` ASC, `?x` ASC");
assert_eq!(args, vec![]);
}
}

View file

@ -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;

View file

@ -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()));
}

View file

@ -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

View file

@ -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`.

View file

@ -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), _)) => {