Implement type requirements/predicates for queries. Fixes #474
This commit is contained in:
parent
ef9f2d9c51
commit
f3dc922571
16 changed files with 478 additions and 106 deletions
|
@ -30,6 +30,7 @@ use errors::{
|
|||
/// the bindings that will be used at execution time.
|
||||
/// When built correctly, `types` is guaranteed to contain the types of `values` -- use
|
||||
/// `QueryInputs::new` or `QueryInputs::with_values` to construct an instance.
|
||||
#[derive(Clone)]
|
||||
pub struct QueryInputs {
|
||||
// These should be crate-private.
|
||||
pub types: BTreeMap<Variable, ValueType>,
|
||||
|
|
|
@ -46,6 +46,8 @@ use mentat_query::{
|
|||
};
|
||||
|
||||
use errors::{
|
||||
Error,
|
||||
ErrorKind,
|
||||
Result,
|
||||
};
|
||||
|
||||
|
@ -214,6 +216,9 @@ pub struct ConjoiningClauses {
|
|||
/// A mapping, similar to `column_bindings`, but used to pull type tags out of the store at runtime.
|
||||
/// If a var isn't unit in `known_types`, it should be present here.
|
||||
pub extracted_types: BTreeMap<Variable, QualifiedAlias>,
|
||||
|
||||
/// Map of variables to the set of type requirements we have for them.
|
||||
required_types: BTreeMap<Variable, ValueType>,
|
||||
}
|
||||
|
||||
impl PartialEq for ConjoiningClauses {
|
||||
|
@ -226,7 +231,8 @@ impl PartialEq for ConjoiningClauses {
|
|||
self.input_variables.eq(&other.input_variables) &&
|
||||
self.value_bindings.eq(&other.value_bindings) &&
|
||||
self.known_types.eq(&other.known_types) &&
|
||||
self.extracted_types.eq(&other.extracted_types)
|
||||
self.extracted_types.eq(&other.extracted_types) &&
|
||||
self.required_types.eq(&other.required_types)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,6 +250,7 @@ impl Debug for ConjoiningClauses {
|
|||
.field("value_bindings", &self.value_bindings)
|
||||
.field("known_types", &self.known_types)
|
||||
.field("extracted_types", &self.extracted_types)
|
||||
.field("required_types", &self.required_types)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
@ -257,6 +264,7 @@ impl Default for ConjoiningClauses {
|
|||
from: vec![],
|
||||
computed_tables: vec![],
|
||||
wheres: ColumnIntersection::default(),
|
||||
required_types: BTreeMap::new(),
|
||||
input_variables: BTreeSet::new(),
|
||||
column_bindings: BTreeMap::new(),
|
||||
value_bindings: BTreeMap::new(),
|
||||
|
@ -320,6 +328,7 @@ impl ConjoiningClauses {
|
|||
value_bindings: self.value_bindings.clone(),
|
||||
known_types: self.known_types.clone(),
|
||||
extracted_types: self.extracted_types.clone(),
|
||||
required_types: self.required_types.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
@ -334,6 +343,7 @@ impl ConjoiningClauses {
|
|||
value_bindings: self.value_bindings.with_intersected_keys(&vars),
|
||||
known_types: self.known_types.with_intersected_keys(&vars),
|
||||
extracted_types: self.extracted_types.with_intersected_keys(&vars),
|
||||
required_types: self.required_types.with_intersected_keys(&vars),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
@ -356,7 +366,7 @@ impl ConjoiningClauses {
|
|||
// Are we also trying to figure out the type of the value when the query runs?
|
||||
// If so, constrain that!
|
||||
if let Some(qa) = self.extracted_types.get(&var) {
|
||||
self.wheres.add_intersection(ColumnConstraint::HasType(qa.0.clone(), vt));
|
||||
self.wheres.add_intersection(ColumnConstraint::has_type(qa.0.clone(), vt));
|
||||
}
|
||||
|
||||
// Finally, store the binding for future use.
|
||||
|
@ -541,6 +551,19 @@ impl ConjoiningClauses {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_type_requirement(&mut self, var: Variable, ty: ValueType) {
|
||||
if let Some(existing) = self.required_types.insert(var.clone(), ty) {
|
||||
// If we already have a required type for `var`, we're empty.
|
||||
if existing != ty {
|
||||
self.mark_known_empty(EmptyBecause::TypeMismatch {
|
||||
var: var.clone(),
|
||||
existing: ValueTypeSet::of_one(existing),
|
||||
desired: ValueTypeSet::of_one(ty)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Like `constrain_var_to_type` but in reverse: this expands the set of types
|
||||
/// with which a variable is associated.
|
||||
///
|
||||
|
@ -848,7 +871,43 @@ impl ConjoiningClauses {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn process_required_types(&mut self) -> Result<()> {
|
||||
// We can't call `mark_known_empty` inside the loop since it would be a
|
||||
// mutable borrow on self while we're iterating over `self.required_types`.
|
||||
// Doing it like this avoids needing to copy `self.required_types`.
|
||||
let mut empty_because: Option<EmptyBecause> = None;
|
||||
for (var, &ty) in self.required_types.iter() {
|
||||
if let Some(&already_known) = self.known_types.get(var) {
|
||||
if already_known.exemplar() == Some(ty) {
|
||||
// If we're already certain the type and the constraint are
|
||||
// the same, then there's no need to constrain anything.
|
||||
continue;
|
||||
}
|
||||
if !already_known.contains(ty) && empty_because.is_none() {
|
||||
// If we know the constraint can't be one of the types
|
||||
// the variable could take, then we know we're empty.
|
||||
empty_because = Some(EmptyBecause::TypeMismatch {
|
||||
var: var.clone(),
|
||||
existing: already_known,
|
||||
desired: ValueTypeSet::of_one(ty)
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
let qa = self.extracted_types
|
||||
.get(&var)
|
||||
.ok_or_else(|| Error::from_kind(ErrorKind::UnboundVariable(var.name())))?;
|
||||
self.wheres.add_intersection(ColumnConstraint::HasType {
|
||||
value: qa.0.clone(),
|
||||
value_type: ty,
|
||||
strict: true,
|
||||
});
|
||||
}
|
||||
if let Some(reason) = empty_because {
|
||||
self.mark_known_empty(reason);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// When a CC has accumulated all patterns, generate value_type_tag entries in `wheres`
|
||||
/// to refine value types for which two things are true:
|
||||
///
|
||||
|
|
|
@ -53,13 +53,14 @@ impl ConjoiningClauses {
|
|||
template.apply_clause(&schema, clause)?;
|
||||
}
|
||||
|
||||
template.expand_column_bindings();
|
||||
template.prune_extracted_types();
|
||||
template.process_required_types()?;
|
||||
|
||||
if template.is_known_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// We are only expanding column bindings here and not pruning extracted types as we are not projecting values.
|
||||
template.expand_column_bindings();
|
||||
|
||||
let subquery = ComputedTable::Subquery(template);
|
||||
|
||||
self.wheres.add_intersection(ColumnConstraint::NotExists(subquery));
|
||||
|
|
|
@ -577,6 +577,7 @@ impl ConjoiningClauses {
|
|||
} else {
|
||||
receptacle.expand_column_bindings();
|
||||
receptacle.prune_extracted_types();
|
||||
receptacle.process_required_types()?;
|
||||
acc.push(receptacle);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -201,7 +201,7 @@ impl ConjoiningClauses {
|
|||
} else {
|
||||
// It must be a keyword.
|
||||
self.constrain_column_to_constant(col.clone(), DatomsColumn::Value, TypedValue::Keyword(kw.clone()));
|
||||
self.wheres.add_intersection(ColumnConstraint::HasType(col.clone(), ValueType::Keyword));
|
||||
self.wheres.add_intersection(ColumnConstraint::has_type(col.clone(), ValueType::Keyword));
|
||||
};
|
||||
},
|
||||
PatternValuePlace::Constant(ref c) => {
|
||||
|
@ -237,7 +237,7 @@ impl ConjoiningClauses {
|
|||
// Because everything we handle here is unambiguous, we generate a single type
|
||||
// restriction from the value type of the typed value.
|
||||
if value_type.is_none() {
|
||||
self.wheres.add_intersection(ColumnConstraint::HasType(col.clone(), typed_value_type));
|
||||
self.wheres.add_intersection(ColumnConstraint::has_type(col.clone(), typed_value_type));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -445,7 +445,7 @@ mod testing {
|
|||
// TODO: implement expand_type_tags.
|
||||
assert_eq!(cc.wheres, vec![
|
||||
ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::Boolean(true))),
|
||||
ColumnConstraint::HasType("datoms00".to_string(), ValueType::Boolean),
|
||||
ColumnConstraint::has_type("datoms00".to_string(), ValueType::Boolean),
|
||||
].into());
|
||||
}
|
||||
|
||||
|
@ -589,7 +589,7 @@ mod testing {
|
|||
// TODO: implement expand_type_tags.
|
||||
assert_eq!(cc.wheres, vec![
|
||||
ColumnConstraint::Equals(d0_v, QueryValue::TypedValue(TypedValue::String(Rc::new("hello".to_string())))),
|
||||
ColumnConstraint::HasType("all_datoms00".to_string(), ValueType::String),
|
||||
ColumnConstraint::has_type("all_datoms00".to_string(), ValueType::String),
|
||||
].into());
|
||||
}
|
||||
|
||||
|
|
|
@ -34,11 +34,26 @@ use types::{
|
|||
Inequality,
|
||||
};
|
||||
|
||||
fn value_type_function_name(s: &str) -> Option<ValueType> {
|
||||
match s {
|
||||
"ref" => Some(ValueType::Ref),
|
||||
"boolean" => Some(ValueType::Boolean),
|
||||
"instant" => Some(ValueType::Instant),
|
||||
"long" => Some(ValueType::Long),
|
||||
"double" => Some(ValueType::Double),
|
||||
"string" => Some(ValueType::String),
|
||||
"keyword" => Some(ValueType::Keyword),
|
||||
"uuid" => Some(ValueType::Uuid),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
/// Application of predicates.
|
||||
impl ConjoiningClauses {
|
||||
/// There are several kinds of predicates in our Datalog:
|
||||
/// - A limited set of binary comparison operators: < > <= >= !=.
|
||||
/// These are converted into SQLite binary comparisons and some type constraints.
|
||||
/// - A set of type requirements constraining their argument to be a specific ValueType
|
||||
/// - In the future, some predicates that are implemented via function calls in SQLite.
|
||||
///
|
||||
/// At present we have implemented only the five built-in comparison binary operators.
|
||||
|
@ -47,6 +62,8 @@ impl ConjoiningClauses {
|
|||
// and ultimately allowing user-specified predicates, we match on the predicate name first.
|
||||
if let Some(op) = Inequality::from_datalog_operator(predicate.operator.0.as_str()) {
|
||||
self.apply_inequality(schema, op, predicate)
|
||||
} else if let Some(ty) = value_type_function_name(predicate.operator.0.as_str()) {
|
||||
self.apply_type_requirement(predicate, ty)
|
||||
} else {
|
||||
bail!(ErrorKind::UnknownFunction(predicate.operator.clone()))
|
||||
}
|
||||
|
@ -59,6 +76,20 @@ impl ConjoiningClauses {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn apply_type_requirement(&mut self, pred: Predicate, ty: ValueType) -> Result<()> {
|
||||
if pred.args.len() != 1 {
|
||||
bail!(ErrorKind::InvalidNumberOfArguments(pred.operator.clone(), pred.args.len(), 1));
|
||||
}
|
||||
let mut args = pred.args.into_iter();
|
||||
|
||||
if let FnArg::Variable(v) = args.next().unwrap() {
|
||||
self.add_type_requirement(v, ty);
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(ErrorKind::InvalidArgument(pred.operator.clone(), "variable".into(), 0))
|
||||
}
|
||||
}
|
||||
|
||||
/// This function:
|
||||
/// - Resolves variables and converts types to those more amenable to SQL.
|
||||
/// - Ensures that the predicate functions name a known operator.
|
||||
|
|
|
@ -185,6 +185,7 @@ pub fn algebrize_with_inputs(schema: &Schema,
|
|||
}
|
||||
cc.expand_column_bindings();
|
||||
cc.prune_extracted_types();
|
||||
cc.process_required_types()?;
|
||||
|
||||
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();
|
||||
|
|
|
@ -334,11 +334,21 @@ pub enum ColumnConstraint {
|
|||
left: QueryValue,
|
||||
right: QueryValue,
|
||||
},
|
||||
HasType(TableAlias, ValueType),
|
||||
HasType {
|
||||
value: TableAlias,
|
||||
value_type: ValueType,
|
||||
strict: bool,
|
||||
},
|
||||
NotExists(ComputedTable),
|
||||
Matches(QualifiedAlias, QueryValue),
|
||||
}
|
||||
|
||||
impl ColumnConstraint {
|
||||
pub fn has_type(value: TableAlias, value_type: ValueType) -> ColumnConstraint {
|
||||
ColumnConstraint::HasType { value, value_type, strict: false }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum ColumnConstraintOrAlternation {
|
||||
Constraint(ColumnConstraint),
|
||||
|
@ -451,8 +461,14 @@ impl Debug for ColumnConstraint {
|
|||
write!(f, "{:?} MATCHES {:?}", qa, thing)
|
||||
},
|
||||
|
||||
&HasType(ref qa, value_type) => {
|
||||
write!(f, "{:?}.value_type_tag = {:?}", qa, value_type)
|
||||
&HasType { ref value, value_type, strict } => {
|
||||
write!(f, "({:?}.value_type_tag = {:?}", value, value_type)?;
|
||||
if strict && value_type == ValueType::Double || value_type == ValueType::Long {
|
||||
write!(f, " AND typeof({:?}) = '{:?}')", value,
|
||||
if value_type == ValueType::Double { "real" } else { "integer" })
|
||||
} else {
|
||||
write!(f, ")")
|
||||
}
|
||||
},
|
||||
&NotExists(ref ct) => {
|
||||
write!(f, "NOT EXISTS {:?}", ct)
|
||||
|
|
|
@ -13,37 +13,24 @@ extern crate mentat_query;
|
|||
extern crate mentat_query_algebrizer;
|
||||
extern crate mentat_query_parser;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
use mentat_core::{
|
||||
Attribute,
|
||||
Entid,
|
||||
Schema,
|
||||
ValueType,
|
||||
};
|
||||
|
||||
use mentat_query_parser::{
|
||||
parse_find_string,
|
||||
};
|
||||
|
||||
use mentat_query::{
|
||||
NamespacedKeyword,
|
||||
};
|
||||
|
||||
use mentat_query_algebrizer::{
|
||||
ConjoiningClauses,
|
||||
algebrize,
|
||||
use utils::{
|
||||
add_attribute,
|
||||
alg,
|
||||
associate_ident,
|
||||
};
|
||||
|
||||
|
||||
// These are helpers that tests use to build Schema instances.
|
||||
fn associate_ident(schema: &mut Schema, i: NamespacedKeyword, e: Entid) {
|
||||
schema.entid_map.insert(e, i.clone());
|
||||
schema.ident_map.insert(i.clone(), e);
|
||||
}
|
||||
|
||||
fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) {
|
||||
schema.attribute_map.insert(e, a);
|
||||
}
|
||||
|
||||
fn prepopulated_schema() -> Schema {
|
||||
let mut schema = Schema::default();
|
||||
associate_ident(&mut schema, NamespacedKeyword::new("foo", "name"), 65);
|
||||
|
@ -80,11 +67,6 @@ fn prepopulated_schema() -> Schema {
|
|||
schema
|
||||
}
|
||||
|
||||
fn alg(schema: &Schema, input: &str) -> ConjoiningClauses {
|
||||
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||
algebrize(schema.into(), parsed).expect("algebrizing to have succeeded").cc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_fulltext() {
|
||||
let schema = prepopulated_schema();
|
||||
|
|
|
@ -13,20 +13,17 @@ extern crate mentat_query;
|
|||
extern crate mentat_query_algebrizer;
|
||||
extern crate mentat_query_parser;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use mentat_core::{
|
||||
Attribute,
|
||||
Entid,
|
||||
Schema,
|
||||
ValueType,
|
||||
TypedValue,
|
||||
};
|
||||
|
||||
use mentat_query_parser::{
|
||||
parse_find_string,
|
||||
};
|
||||
|
||||
use mentat_query::{
|
||||
NamespacedKeyword,
|
||||
PlainSymbol,
|
||||
|
@ -35,26 +32,19 @@ use mentat_query::{
|
|||
|
||||
use mentat_query_algebrizer::{
|
||||
BindingError,
|
||||
ConjoiningClauses,
|
||||
ComputedTable,
|
||||
Error,
|
||||
ErrorKind,
|
||||
QueryInputs,
|
||||
algebrize,
|
||||
algebrize_with_inputs,
|
||||
};
|
||||
|
||||
// These are helpers that tests use to build Schema instances.
|
||||
#[cfg(test)]
|
||||
fn associate_ident(schema: &mut Schema, i: NamespacedKeyword, e: Entid) {
|
||||
schema.entid_map.insert(e, i.clone());
|
||||
schema.ident_map.insert(i.clone(), e);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) {
|
||||
schema.attribute_map.insert(e, a);
|
||||
}
|
||||
use utils::{
|
||||
add_attribute,
|
||||
alg,
|
||||
associate_ident,
|
||||
bails,
|
||||
bails_with_inputs,
|
||||
};
|
||||
|
||||
fn prepopulated_schema() -> Schema {
|
||||
let mut schema = Schema::default();
|
||||
|
@ -91,21 +81,6 @@ fn prepopulated_schema() -> Schema {
|
|||
schema
|
||||
}
|
||||
|
||||
fn bails(schema: &Schema, input: &str) -> Error {
|
||||
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||
algebrize(schema.into(), parsed).expect_err("algebrize to have failed")
|
||||
}
|
||||
|
||||
fn bails_with_inputs(schema: &Schema, input: &str, inputs: QueryInputs) -> Error {
|
||||
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||
algebrize_with_inputs(schema, parsed, 0, inputs).expect_err("algebrize to have failed")
|
||||
}
|
||||
|
||||
fn alg(schema: &Schema, input: &str) -> ConjoiningClauses {
|
||||
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||
algebrize(schema.into(), parsed).expect("algebrizing to have succeeded").cc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ground_doesnt_bail_for_type_conflicts() {
|
||||
// We know `?x` to be a ref, but we're attempting to ground it to a Double.
|
||||
|
|
|
@ -13,18 +13,15 @@ extern crate mentat_query;
|
|||
extern crate mentat_query_algebrizer;
|
||||
extern crate mentat_query_parser;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
use mentat_core::{
|
||||
Attribute,
|
||||
Entid,
|
||||
Schema,
|
||||
ValueType,
|
||||
ValueTypeSet,
|
||||
};
|
||||
|
||||
use mentat_query_parser::{
|
||||
parse_find_string,
|
||||
};
|
||||
|
||||
use mentat_query::{
|
||||
NamespacedKeyword,
|
||||
PlainSymbol,
|
||||
|
@ -32,24 +29,16 @@ use mentat_query::{
|
|||
};
|
||||
|
||||
use mentat_query_algebrizer::{
|
||||
ConjoiningClauses,
|
||||
EmptyBecause,
|
||||
Error,
|
||||
ErrorKind,
|
||||
algebrize,
|
||||
};
|
||||
|
||||
// These are helpers that tests use to build Schema instances.
|
||||
#[cfg(test)]
|
||||
fn associate_ident(schema: &mut Schema, i: NamespacedKeyword, e: Entid) {
|
||||
schema.entid_map.insert(e, i.clone());
|
||||
schema.ident_map.insert(i.clone(), e);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) {
|
||||
schema.attribute_map.insert(e, a);
|
||||
}
|
||||
use utils::{
|
||||
add_attribute,
|
||||
alg,
|
||||
associate_ident,
|
||||
bails,
|
||||
};
|
||||
|
||||
fn prepopulated_schema() -> Schema {
|
||||
let mut schema = Schema::default();
|
||||
|
@ -68,16 +57,6 @@ fn prepopulated_schema() -> Schema {
|
|||
schema
|
||||
}
|
||||
|
||||
fn bails(schema: &Schema, input: &str) -> Error {
|
||||
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||
algebrize(schema.into(), parsed).expect_err("algebrize to have failed")
|
||||
}
|
||||
|
||||
fn alg(schema: &Schema, input: &str) -> ConjoiningClauses {
|
||||
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||
algebrize(schema.into(), parsed).expect("algebrizing to have succeeded").cc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_instant_predicates_require_instants() {
|
||||
let schema = prepopulated_schema();
|
||||
|
|
80
query-algebrizer/tests/type_reqs.rs
Normal file
80
query-algebrizer/tests/type_reqs.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2016 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
extern crate mentat_core;
|
||||
extern crate mentat_query;
|
||||
extern crate mentat_query_algebrizer;
|
||||
extern crate mentat_query_parser;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
use utils::{
|
||||
alg,
|
||||
SchemaBuilder,
|
||||
bails,
|
||||
};
|
||||
|
||||
use mentat_core::{
|
||||
Schema,
|
||||
ValueType,
|
||||
};
|
||||
|
||||
fn prepopulated_schema() -> Schema {
|
||||
SchemaBuilder::new()
|
||||
.define_simple_attr("test", "boolean", ValueType::Boolean, false)
|
||||
.define_simple_attr("test", "long", ValueType::Long, false)
|
||||
.define_simple_attr("test", "double", ValueType::Double, false)
|
||||
.define_simple_attr("test", "string", ValueType::String, false)
|
||||
.define_simple_attr("test", "keyword", ValueType::Keyword, false)
|
||||
.define_simple_attr("test", "uuid", ValueType::Uuid, false)
|
||||
.define_simple_attr("test", "instant", ValueType::Instant, false)
|
||||
.define_simple_attr("test", "ref", ValueType::Ref, false)
|
||||
.schema
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_known() {
|
||||
let type_names = [
|
||||
"boolean",
|
||||
"long",
|
||||
"double",
|
||||
"string",
|
||||
"keyword",
|
||||
"uuid",
|
||||
"instant",
|
||||
"ref",
|
||||
];
|
||||
let schema = prepopulated_schema();
|
||||
for known_type in type_names.iter() {
|
||||
for required in type_names.iter() {
|
||||
let q = format!("[:find ?e :where [?e :test/{} ?v] [({} ?v)]]",
|
||||
known_type, required);
|
||||
println!("Query: {}", q);
|
||||
let cc = alg(&schema, &q);
|
||||
// It should only be empty if the known type and our requirement differ.
|
||||
assert_eq!(cc.empty_because.is_some(), known_type != required,
|
||||
"known_type = {}; required = {}", known_type, required);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple() {
|
||||
let schema = prepopulated_schema();
|
||||
let q = "[:find ?e :where [?e _ ?v] [(long ?v)] [(double ?v)]]";
|
||||
let cc = alg(&schema, &q);
|
||||
assert!(cc.empty_because.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unbound() {
|
||||
let schema = prepopulated_schema();
|
||||
bails(&schema, "[:find ?e :where [(string ?e)]]");
|
||||
}
|
98
query-algebrizer/tests/utils/mod.rs
Normal file
98
query-algebrizer/tests/utils/mod.rs
Normal file
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2018 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
use mentat_core::{
|
||||
Attribute,
|
||||
Entid,
|
||||
Schema,
|
||||
ValueType,
|
||||
};
|
||||
|
||||
use mentat_query_parser::{
|
||||
parse_find_string,
|
||||
};
|
||||
|
||||
use mentat_query::{
|
||||
NamespacedKeyword,
|
||||
};
|
||||
|
||||
use mentat_query_algebrizer::{
|
||||
algebrize,
|
||||
algebrize_with_inputs,
|
||||
ConjoiningClauses,
|
||||
Error,
|
||||
QueryInputs,
|
||||
};
|
||||
|
||||
// Common utility functions used in multiple test files. Note: Import this with
|
||||
// `pub mod utils` (not `mod utils`), or you'll get spurious unused function
|
||||
// warnings when functions exist in this file but are only used by modules that
|
||||
// don't import with `pub` (yes, this is annoying).
|
||||
|
||||
// These are helpers that tests use to build Schema instances.
|
||||
pub fn associate_ident(schema: &mut Schema, i: NamespacedKeyword, e: Entid) {
|
||||
schema.entid_map.insert(e, i.clone());
|
||||
schema.ident_map.insert(i.clone(), e);
|
||||
}
|
||||
|
||||
pub fn add_attribute(schema: &mut Schema, e: Entid, a: Attribute) {
|
||||
schema.attribute_map.insert(e, a);
|
||||
}
|
||||
|
||||
pub struct SchemaBuilder {
|
||||
pub schema: Schema,
|
||||
pub counter: Entid,
|
||||
}
|
||||
|
||||
impl SchemaBuilder {
|
||||
pub fn new() -> SchemaBuilder {
|
||||
SchemaBuilder {
|
||||
schema: Schema::default(),
|
||||
counter: 65
|
||||
}
|
||||
}
|
||||
|
||||
pub fn define_attr(mut self, kw: NamespacedKeyword, attr: Attribute) -> Self {
|
||||
associate_ident(&mut self.schema, kw, self.counter);
|
||||
add_attribute(&mut self.schema, self.counter, attr);
|
||||
self.counter += 1;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn define_simple_attr<T>(self,
|
||||
keyword_ns: T,
|
||||
keyword_name: T,
|
||||
value_type: ValueType,
|
||||
multival: bool) -> Self
|
||||
where T: Into<String>
|
||||
{
|
||||
self.define_attr(NamespacedKeyword::new(keyword_ns, keyword_name), Attribute {
|
||||
value_type,
|
||||
multival,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bails(schema: &Schema, input: &str) -> Error {
|
||||
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||
algebrize(schema.into(), parsed).expect_err("algebrize to have failed")
|
||||
}
|
||||
|
||||
pub fn bails_with_inputs(schema: &Schema, input: &str, inputs: QueryInputs) -> Error {
|
||||
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||
algebrize_with_inputs(schema, parsed, 0, inputs).expect_err("algebrize to have failed")
|
||||
}
|
||||
|
||||
pub fn alg(schema: &Schema, input: &str) -> ConjoiningClauses {
|
||||
let parsed = parse_find_string(input).expect("query input to have parsed");
|
||||
algebrize(schema.into(), parsed).expect("algebrizing to have succeeded").cc
|
||||
}
|
||||
|
|
@ -105,9 +105,22 @@ pub enum Constraint {
|
|||
},
|
||||
NotExists {
|
||||
subquery: TableOrSubquery,
|
||||
},
|
||||
TypeCheck {
|
||||
value: ColumnOrExpression,
|
||||
datatype: SQLDatatype
|
||||
}
|
||||
}
|
||||
|
||||
/// Type safe representation of the possible return values from `typeof`
|
||||
pub enum SQLDatatype {
|
||||
Null, // "null"
|
||||
Integer, // "integer"
|
||||
Real, // "real"
|
||||
Text, // "text"
|
||||
Blob, // "blob"
|
||||
}
|
||||
|
||||
impl Constraint {
|
||||
pub fn not_equal(left: ColumnOrExpression, right: ColumnOrExpression) -> Constraint {
|
||||
Constraint::Infix {
|
||||
|
@ -313,6 +326,19 @@ impl QueryFragment for Op {
|
|||
}
|
||||
}
|
||||
|
||||
impl QueryFragment for SQLDatatype {
|
||||
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
|
||||
out.push_sql(match *self {
|
||||
SQLDatatype::Null => "'null'",
|
||||
SQLDatatype::Integer => "'integer'",
|
||||
SQLDatatype::Real => "'real'",
|
||||
SQLDatatype::Text => "'text'",
|
||||
SQLDatatype::Blob => "'blob'",
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryFragment for Constraint {
|
||||
fn push_sql(&self, out: &mut QueryBuilder) -> BuildQueryResult {
|
||||
use self::Constraint::*;
|
||||
|
@ -367,7 +393,14 @@ impl QueryFragment for Constraint {
|
|||
subquery.push_sql(out)?;
|
||||
out.push_sql(")");
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
&TypeCheck { ref value, ref datatype } => {
|
||||
out.push_sql("typeof(");
|
||||
value.push_sql(out)?;
|
||||
out.push_sql(") = ");
|
||||
datatype.push_sql(out)?;
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ use mentat_query_sql::{
|
|||
ProjectedColumn,
|
||||
Projection,
|
||||
SelectQuery,
|
||||
SQLDatatype,
|
||||
TableList,
|
||||
TableOrSubquery,
|
||||
Values,
|
||||
|
@ -157,10 +158,30 @@ impl ToConstraint for ColumnConstraint {
|
|||
right: right.into(),
|
||||
}
|
||||
},
|
||||
|
||||
HasType(table, value_type) => {
|
||||
let column = QualifiedAlias::new(table, DatomsColumn::ValueTypeTag).to_column();
|
||||
Constraint::equal(column, ColumnOrExpression::Integer(value_type.value_type_tag()))
|
||||
HasType { value: table, value_type, strict } => {
|
||||
let type_column = QualifiedAlias::new(table.clone(), DatomsColumn::ValueTypeTag).to_column();
|
||||
let loose = Constraint::equal(type_column,
|
||||
ColumnOrExpression::Integer(value_type.value_type_tag()));
|
||||
if !strict || (value_type != ValueType::Long && value_type != ValueType::Double) {
|
||||
loose
|
||||
} else {
|
||||
// HasType has requested that we check for strict equality, and we're
|
||||
// checking a ValueType where that makes a difference (a numeric type).
|
||||
let val_column = QualifiedAlias::new(table, DatomsColumn::Value).to_column();
|
||||
Constraint::And {
|
||||
constraints: vec![
|
||||
loose,
|
||||
Constraint::TypeCheck {
|
||||
value: val_column,
|
||||
datatype: match value_type {
|
||||
ValueType::Long => SQLDatatype::Integer,
|
||||
ValueType::Double => SQLDatatype::Real,
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
NotExists(computed_table) => {
|
||||
|
|
|
@ -15,6 +15,7 @@ extern crate mentat;
|
|||
extern crate mentat_core;
|
||||
extern crate mentat_db;
|
||||
extern crate mentat_query_algebrizer; // For errors.
|
||||
extern crate rusqlite;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -25,9 +26,9 @@ use mentat_core::{
|
|||
HasSchema,
|
||||
KnownEntid,
|
||||
TypedValue,
|
||||
ValueType,
|
||||
Utc,
|
||||
Uuid,
|
||||
ValueType,
|
||||
};
|
||||
|
||||
use mentat::{
|
||||
|
@ -501,3 +502,96 @@ fn test_lookup() {
|
|||
let fetched_many = conn.lookup_value_for_attribute(&c, *entid, &foo_many).unwrap().unwrap();
|
||||
assert!(two_longs.contains(&fetched_many));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_reqs() {
|
||||
let mut c = new_connection("").expect("Couldn't open conn.");
|
||||
let mut conn = Conn::connect(&mut c).expect("Couldn't open DB.");
|
||||
|
||||
conn.transact(&mut c, r#"[
|
||||
{:db/ident :test/boolean :db/valueType :db.type/boolean :db/cardinality :db.cardinality/one}
|
||||
{:db/ident :test/long :db/valueType :db.type/long :db/cardinality :db.cardinality/one}
|
||||
{:db/ident :test/double :db/valueType :db.type/double :db/cardinality :db.cardinality/one}
|
||||
{:db/ident :test/string :db/valueType :db.type/string :db/cardinality :db.cardinality/one}
|
||||
{:db/ident :test/keyword :db/valueType :db.type/keyword :db/cardinality :db.cardinality/one}
|
||||
{:db/ident :test/uuid :db/valueType :db.type/uuid :db/cardinality :db.cardinality/one}
|
||||
{:db/ident :test/instant :db/valueType :db.type/instant :db/cardinality :db.cardinality/one}
|
||||
{:db/ident :test/ref :db/valueType :db.type/ref :db/cardinality :db.cardinality/one}
|
||||
]"#).unwrap();
|
||||
|
||||
conn.transact(&mut c, r#"[
|
||||
{:test/boolean true
|
||||
:test/long 33
|
||||
:test/double 1.4
|
||||
:test/string "foo"
|
||||
:test/keyword :foo/bar
|
||||
:test/uuid #uuid "12341234-1234-1234-1234-123412341234"
|
||||
:test/instant #inst "2018-01-01T11:00:00.000Z"
|
||||
:test/ref 1}
|
||||
]"#).unwrap();
|
||||
|
||||
let eid_query = r#"[:find ?eid :where [?eid :test/string "foo"]]"#;
|
||||
|
||||
let res = conn.q_once(&mut c, eid_query, None).unwrap();
|
||||
|
||||
let entid = match res {
|
||||
QueryResults::Rel(ref vs) if vs.len() == 1 && vs[0].len() == 1 && vs[0][0].matches_type(ValueType::Ref) =>
|
||||
if let TypedValue::Ref(eid) = vs[0][0] {
|
||||
eid
|
||||
} else {
|
||||
// Already checked this.
|
||||
unreachable!();
|
||||
}
|
||||
unexpected => {
|
||||
panic!("Query to get the entity id returned unexpected result {:?}", unexpected);
|
||||
}
|
||||
};
|
||||
|
||||
let type_names = &[
|
||||
"boolean",
|
||||
"long",
|
||||
"double",
|
||||
"string",
|
||||
"keyword",
|
||||
"uuid",
|
||||
"instant",
|
||||
"ref",
|
||||
];
|
||||
|
||||
let entid_binding = QueryInputs::with_value_sequence(vec![
|
||||
(Variable::from_valid_name("?e"), TypedValue::Ref(entid)),
|
||||
]);
|
||||
|
||||
for name in type_names {
|
||||
let q = format!("[:find [?v ...] :in ?e :where [?e _ ?v] [({} ?v)]]", name);
|
||||
let results = conn.q_once(&mut c, &q, entid_binding.clone()).unwrap();
|
||||
match results {
|
||||
QueryResults::Coll(vals) => {
|
||||
assert_eq!(vals.len(), 1, "Query should find exactly 1 item");
|
||||
},
|
||||
v => {
|
||||
panic!("Query returned unexpected type: {:?}", v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conn.transact(&mut c, r#"[
|
||||
{:db/ident :test/long2 :db/valueType :db.type/long :db/cardinality :db.cardinality/one}
|
||||
]"#).unwrap();
|
||||
|
||||
conn.transact(&mut c, &format!("[[:db/add {} :test/long2 5]]", entid)).unwrap();
|
||||
let longs_query = r#"[:find [?v ...]
|
||||
:order (asc ?v)
|
||||
:in ?e
|
||||
:where [?e _ ?v] [(long ?v)]]"#;
|
||||
|
||||
let res = conn.q_once(&mut c, longs_query, entid_binding.clone()).unwrap();
|
||||
match res {
|
||||
QueryResults::Coll(vals) => {
|
||||
assert_eq!(vals, vec![TypedValue::Long(5), TypedValue::Long(33)])
|
||||
},
|
||||
v => {
|
||||
panic!("Query returned unexpected type: {:?}", v);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue