(query) Implement tx-log API: (tx-ids ...) and (tx-data ...) functions.
`tx-ids` allows to enumerate transaction IDs efficiently. `tx-data` allows to extract transaction log data efficiently. We might eventually allow to filter by impacted attribute sets as well.
This commit is contained in:
parent
e532614908
commit
c8da4be38f
9 changed files with 688 additions and 0 deletions
|
@ -84,6 +84,7 @@ mod resolve;
|
||||||
|
|
||||||
mod ground;
|
mod ground;
|
||||||
mod fulltext;
|
mod fulltext;
|
||||||
|
mod tx_log_api;
|
||||||
mod where_fn;
|
mod where_fn;
|
||||||
|
|
||||||
use validate::{
|
use validate::{
|
||||||
|
@ -449,6 +450,10 @@ impl ConjoiningClauses {
|
||||||
self.constrain_column_to_constant(table, column, bound_val);
|
self.constrain_column_to_constant(table, column, bound_val);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Column::Transactions(_) => {
|
||||||
|
self.constrain_column_to_constant(table, column, bound_val);
|
||||||
|
},
|
||||||
|
|
||||||
Column::Fulltext(FulltextColumn::Rowid) |
|
Column::Fulltext(FulltextColumn::Rowid) |
|
||||||
Column::Fulltext(FulltextColumn::Text) => {
|
Column::Fulltext(FulltextColumn::Text) => {
|
||||||
// We never expose `rowid` via queries. We do expose `text`, but only
|
// We never expose `rowid` via queries. We do expose `text`, but only
|
||||||
|
|
|
@ -137,6 +137,14 @@ impl ConjoiningClauses {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take a transaction ID function argument and turn it into a `QueryValue` suitable for use in
|
||||||
|
/// a concrete constraint.
|
||||||
|
pub(crate) fn resolve_tx_argument(&mut self, schema: &Schema, function: &PlainSymbol, position: usize, arg: FnArg) -> Result<QueryValue> {
|
||||||
|
// Under the hood there's nothing special about a transaction ID -- it's just another ref.
|
||||||
|
// In the future, we might handle instants specially.
|
||||||
|
self.resolve_ref_argument(schema, function, position, arg)
|
||||||
|
}
|
||||||
|
|
||||||
/// Take a function argument and turn it into a `QueryValue` suitable for use in a concrete
|
/// Take a function argument and turn it into a `QueryValue` suitable for use in a concrete
|
||||||
/// constraint.
|
/// constraint.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
394
query-algebrizer/src/clauses/tx_log_api.rs
Normal file
394
query-algebrizer/src/clauses/tx_log_api.rs
Normal file
|
@ -0,0 +1,394 @@
|
||||||
|
// 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::{
|
||||||
|
ValueType,
|
||||||
|
};
|
||||||
|
|
||||||
|
use mentat_query::{
|
||||||
|
Binding,
|
||||||
|
FnArg,
|
||||||
|
SrcVar,
|
||||||
|
VariableOrPlaceholder,
|
||||||
|
WhereFn,
|
||||||
|
};
|
||||||
|
|
||||||
|
use clauses::{
|
||||||
|
ConjoiningClauses,
|
||||||
|
};
|
||||||
|
|
||||||
|
use errors::{
|
||||||
|
BindingError,
|
||||||
|
ErrorKind,
|
||||||
|
Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
use types::{
|
||||||
|
Column,
|
||||||
|
ColumnConstraint,
|
||||||
|
DatomsTable,
|
||||||
|
Inequality,
|
||||||
|
QualifiedAlias,
|
||||||
|
QueryValue,
|
||||||
|
SourceAlias,
|
||||||
|
TransactionsColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
use Known;
|
||||||
|
|
||||||
|
impl ConjoiningClauses {
|
||||||
|
// Log in Query: tx-ids and tx-data
|
||||||
|
//
|
||||||
|
// The log API includes two convenience functions that are available within query. The tx-ids
|
||||||
|
// function takes arguments similar to txRange above, but returns a collection of transaction
|
||||||
|
// entity ids. You will typically use the collection binding form [?tx …] to capture the
|
||||||
|
// results.
|
||||||
|
//
|
||||||
|
// [(tx-ids ?log ?tx1 ?tx2) [?tx ...]]
|
||||||
|
//
|
||||||
|
// TODO: handle tx1 == 0 (more generally, tx1 < bootstrap::TX0) specially (no after constraint).
|
||||||
|
// TODO: allow tx2 to be missing (no before constraint).
|
||||||
|
// TODO: allow txK arguments to be instants.
|
||||||
|
// TODO: allow arbitrary additional attribute arguments that restrict the tx-ids to those
|
||||||
|
// transactions that impact one of the given attributes.
|
||||||
|
pub(crate) fn apply_tx_ids(&mut self, known: Known, where_fn: WhereFn) -> Result<()> {
|
||||||
|
if where_fn.args.len() != 3 {
|
||||||
|
bail!(ErrorKind::InvalidNumberOfArguments(where_fn.operator.clone(), where_fn.args.len(), 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
if where_fn.binding.is_empty() {
|
||||||
|
// The binding must introduce at least one bound variable.
|
||||||
|
bail!(ErrorKind::InvalidBinding(where_fn.operator.clone(), BindingError::NoBoundVariable));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !where_fn.binding.is_valid() {
|
||||||
|
// The binding must not duplicate bound variables.
|
||||||
|
bail!(ErrorKind::InvalidBinding(where_fn.operator.clone(), BindingError::RepeatedBoundVariable));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should have exactly one binding. Destructure it now.
|
||||||
|
let tx_var = match where_fn.binding {
|
||||||
|
Binding::BindRel(bindings) => {
|
||||||
|
let bindings_count = bindings.len();
|
||||||
|
if bindings_count != 1 {
|
||||||
|
bail!(ErrorKind::InvalidBinding(where_fn.operator.clone(),
|
||||||
|
BindingError::InvalidNumberOfBindings {
|
||||||
|
number: bindings_count,
|
||||||
|
expected: 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
match bindings.into_iter().next().unwrap() {
|
||||||
|
VariableOrPlaceholder::Placeholder => unreachable!("binding.is_empty()!"),
|
||||||
|
VariableOrPlaceholder::Variable(v) => v,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Binding::BindColl(v) => v,
|
||||||
|
Binding::BindScalar(_) |
|
||||||
|
Binding::BindTuple(_) => {
|
||||||
|
bail!(ErrorKind::InvalidBinding(where_fn.operator.clone(), BindingError::ExpectedBindRelOrBindColl))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut args = where_fn.args.into_iter();
|
||||||
|
|
||||||
|
// TODO: process source variables.
|
||||||
|
match args.next().unwrap() {
|
||||||
|
FnArg::SrcVar(SrcVar::DefaultSrc) => {},
|
||||||
|
_ => bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "source variable", 0)),
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx1 = self.resolve_tx_argument(&known.schema, &where_fn.operator, 1, args.next().unwrap())?;
|
||||||
|
let tx2 = self.resolve_tx_argument(&known.schema, &where_fn.operator, 2, args.next().unwrap())?;
|
||||||
|
|
||||||
|
let transactions = self.next_alias_for_table(DatomsTable::Transactions);
|
||||||
|
|
||||||
|
self.from.push(SourceAlias(DatomsTable::Transactions, transactions.clone()));
|
||||||
|
|
||||||
|
// Bound variable must be a ref.
|
||||||
|
self.constrain_var_to_type(tx_var.clone(), ValueType::Ref);
|
||||||
|
if self.is_known_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bind_column_to_var(known.schema, transactions.clone(), TransactionsColumn::Tx, tx_var.clone());
|
||||||
|
|
||||||
|
let after_constraint = ColumnConstraint::Inequality {
|
||||||
|
operator: Inequality::LessThanOrEquals,
|
||||||
|
left: tx1,
|
||||||
|
right: QueryValue::Column(QualifiedAlias(transactions.clone(), Column::Transactions(TransactionsColumn::Tx))),
|
||||||
|
};
|
||||||
|
self.wheres.add_intersection(after_constraint);
|
||||||
|
|
||||||
|
let before_constraint = ColumnConstraint::Inequality {
|
||||||
|
operator: Inequality::LessThan,
|
||||||
|
left: QueryValue::Column(QualifiedAlias(transactions.clone(), Column::Transactions(TransactionsColumn::Tx))),
|
||||||
|
right: tx2,
|
||||||
|
};
|
||||||
|
self.wheres.add_intersection(before_constraint);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn apply_tx_data(&mut self, known: Known, where_fn: WhereFn) -> Result<()> {
|
||||||
|
if where_fn.args.len() != 2 {
|
||||||
|
bail!(ErrorKind::InvalidNumberOfArguments(where_fn.operator.clone(), where_fn.args.len(), 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
if where_fn.binding.is_empty() {
|
||||||
|
// The binding must introduce at least one bound variable.
|
||||||
|
bail!(ErrorKind::InvalidBinding(where_fn.operator.clone(), BindingError::NoBoundVariable));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !where_fn.binding.is_valid() {
|
||||||
|
// The binding must not duplicate bound variables.
|
||||||
|
bail!(ErrorKind::InvalidBinding(where_fn.operator.clone(), BindingError::RepeatedBoundVariable));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should have at most five bindings. Destructure them now.
|
||||||
|
let bindings = match where_fn.binding {
|
||||||
|
Binding::BindRel(bindings) => {
|
||||||
|
let bindings_count = bindings.len();
|
||||||
|
if bindings_count < 1 || bindings_count > 5 {
|
||||||
|
bail!(ErrorKind::InvalidBinding(where_fn.operator.clone(),
|
||||||
|
BindingError::InvalidNumberOfBindings {
|
||||||
|
number: bindings.len(),
|
||||||
|
expected: 5,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
bindings
|
||||||
|
},
|
||||||
|
Binding::BindScalar(_) |
|
||||||
|
Binding::BindTuple(_) |
|
||||||
|
Binding::BindColl(_) => bail!(ErrorKind::InvalidBinding(where_fn.operator.clone(), BindingError::ExpectedBindRel)),
|
||||||
|
};
|
||||||
|
let mut bindings = bindings.into_iter();
|
||||||
|
let b_e = bindings.next().unwrap_or(VariableOrPlaceholder::Placeholder);
|
||||||
|
let b_a = bindings.next().unwrap_or(VariableOrPlaceholder::Placeholder);
|
||||||
|
let b_v = bindings.next().unwrap_or(VariableOrPlaceholder::Placeholder);
|
||||||
|
let b_tx = bindings.next().unwrap_or(VariableOrPlaceholder::Placeholder);
|
||||||
|
let b_op = bindings.next().unwrap_or(VariableOrPlaceholder::Placeholder);
|
||||||
|
|
||||||
|
let mut args = where_fn.args.into_iter();
|
||||||
|
|
||||||
|
// TODO: process source variables.
|
||||||
|
match args.next().unwrap() {
|
||||||
|
FnArg::SrcVar(SrcVar::DefaultSrc) => {},
|
||||||
|
_ => bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "source variable", 0)),
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx = self.resolve_tx_argument(&known.schema, &where_fn.operator, 1, args.next().unwrap())?;
|
||||||
|
|
||||||
|
let transactions = self.next_alias_for_table(DatomsTable::Transactions);
|
||||||
|
|
||||||
|
self.from.push(SourceAlias(DatomsTable::Transactions, transactions.clone()));
|
||||||
|
|
||||||
|
let tx_constraint = ColumnConstraint::Equals(
|
||||||
|
QualifiedAlias(transactions.clone(), Column::Transactions(TransactionsColumn::Tx)),
|
||||||
|
tx);
|
||||||
|
self.wheres.add_intersection(tx_constraint);
|
||||||
|
|
||||||
|
if let VariableOrPlaceholder::Variable(ref var) = b_e {
|
||||||
|
// It must be a ref.
|
||||||
|
self.constrain_var_to_type(var.clone(), ValueType::Ref);
|
||||||
|
if self.is_known_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bind_column_to_var(known.schema, transactions.clone(), TransactionsColumn::Entity, var.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let VariableOrPlaceholder::Variable(ref var) = b_a {
|
||||||
|
// It must be a ref.
|
||||||
|
self.constrain_var_to_type(var.clone(), ValueType::Ref);
|
||||||
|
if self.is_known_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bind_column_to_var(known.schema, transactions.clone(), TransactionsColumn::Attribute, var.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let VariableOrPlaceholder::Variable(ref var) = b_v {
|
||||||
|
self.bind_column_to_var(known.schema, transactions.clone(), TransactionsColumn::Value, var.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let VariableOrPlaceholder::Variable(ref var) = b_tx {
|
||||||
|
// It must be a ref.
|
||||||
|
self.constrain_var_to_type(var.clone(), ValueType::Ref);
|
||||||
|
if self.is_known_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this might be a programming error if var is our tx argument. Perhaps we can be
|
||||||
|
// helpful in that case.
|
||||||
|
self.bind_column_to_var(known.schema, transactions.clone(), TransactionsColumn::Tx, var.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let VariableOrPlaceholder::Variable(ref var) = b_op {
|
||||||
|
// It must be a boolean.
|
||||||
|
self.constrain_var_to_type(var.clone(), ValueType::Boolean);
|
||||||
|
if self.is_known_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bind_column_to_var(known.schema, transactions.clone(), TransactionsColumn::Added, var.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod testing {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use mentat_core::{
|
||||||
|
Schema,
|
||||||
|
TypedValue,
|
||||||
|
ValueType,
|
||||||
|
};
|
||||||
|
|
||||||
|
use mentat_query::{
|
||||||
|
Binding,
|
||||||
|
FnArg,
|
||||||
|
PlainSymbol,
|
||||||
|
Variable,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_apply_tx_ids() {
|
||||||
|
let mut cc = ConjoiningClauses::default();
|
||||||
|
let schema = Schema::default();
|
||||||
|
|
||||||
|
let known = Known::for_schema(&schema);
|
||||||
|
|
||||||
|
let op = PlainSymbol::new("tx-ids");
|
||||||
|
cc.apply_tx_ids(known, WhereFn {
|
||||||
|
operator: op,
|
||||||
|
args: vec![
|
||||||
|
FnArg::SrcVar(SrcVar::DefaultSrc),
|
||||||
|
FnArg::EntidOrInteger(1000),
|
||||||
|
FnArg::EntidOrInteger(2000),
|
||||||
|
],
|
||||||
|
binding: Binding::BindRel(vec![VariableOrPlaceholder::Variable(Variable::from_valid_name("?tx")),
|
||||||
|
]),
|
||||||
|
}).expect("to be able to apply_tx_ids");
|
||||||
|
|
||||||
|
assert!(!cc.is_known_empty());
|
||||||
|
|
||||||
|
// Finally, expand column bindings.
|
||||||
|
cc.expand_column_bindings();
|
||||||
|
assert!(!cc.is_known_empty());
|
||||||
|
|
||||||
|
let clauses = cc.wheres;
|
||||||
|
assert_eq!(clauses.len(), 2);
|
||||||
|
|
||||||
|
assert_eq!(clauses.0[0],
|
||||||
|
ColumnConstraint::Inequality {
|
||||||
|
operator: Inequality::LessThanOrEquals,
|
||||||
|
left: QueryValue::TypedValue(TypedValue::Ref(1000)),
|
||||||
|
right: QueryValue::Column(QualifiedAlias("transactions00".to_string(), Column::Transactions(TransactionsColumn::Tx))),
|
||||||
|
}.into());
|
||||||
|
|
||||||
|
assert_eq!(clauses.0[1],
|
||||||
|
ColumnConstraint::Inequality {
|
||||||
|
operator: Inequality::LessThan,
|
||||||
|
left: QueryValue::Column(QualifiedAlias("transactions00".to_string(), Column::Transactions(TransactionsColumn::Tx))),
|
||||||
|
right: QueryValue::TypedValue(TypedValue::Ref(2000)),
|
||||||
|
}.into());
|
||||||
|
|
||||||
|
let bindings = cc.column_bindings;
|
||||||
|
assert_eq!(bindings.len(), 1);
|
||||||
|
|
||||||
|
assert_eq!(bindings.get(&Variable::from_valid_name("?tx")).expect("column binding for ?tx").clone(),
|
||||||
|
vec![QualifiedAlias("transactions00".to_string(), Column::Transactions(TransactionsColumn::Tx))]);
|
||||||
|
|
||||||
|
let known_types = cc.known_types;
|
||||||
|
assert_eq!(known_types.len(), 1);
|
||||||
|
|
||||||
|
assert_eq!(known_types.get(&Variable::from_valid_name("?tx")).expect("known types for ?tx").clone(),
|
||||||
|
vec![ValueType::Ref].into_iter().collect());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_apply_tx_data() {
|
||||||
|
let mut cc = ConjoiningClauses::default();
|
||||||
|
let schema = Schema::default();
|
||||||
|
|
||||||
|
let known = Known::for_schema(&schema);
|
||||||
|
|
||||||
|
let op = PlainSymbol::new("tx-data");
|
||||||
|
cc.apply_tx_data(known, WhereFn {
|
||||||
|
operator: op,
|
||||||
|
args: vec![
|
||||||
|
FnArg::SrcVar(SrcVar::DefaultSrc),
|
||||||
|
FnArg::EntidOrInteger(1000),
|
||||||
|
],
|
||||||
|
binding: Binding::BindRel(vec![
|
||||||
|
VariableOrPlaceholder::Variable(Variable::from_valid_name("?e")),
|
||||||
|
VariableOrPlaceholder::Variable(Variable::from_valid_name("?a")),
|
||||||
|
VariableOrPlaceholder::Variable(Variable::from_valid_name("?v")),
|
||||||
|
VariableOrPlaceholder::Variable(Variable::from_valid_name("?tx")),
|
||||||
|
VariableOrPlaceholder::Variable(Variable::from_valid_name("?added")),
|
||||||
|
]),
|
||||||
|
}).expect("to be able to apply_tx_data");
|
||||||
|
|
||||||
|
assert!(!cc.is_known_empty());
|
||||||
|
|
||||||
|
// Finally, expand column bindings.
|
||||||
|
cc.expand_column_bindings();
|
||||||
|
assert!(!cc.is_known_empty());
|
||||||
|
|
||||||
|
let clauses = cc.wheres;
|
||||||
|
assert_eq!(clauses.len(), 1);
|
||||||
|
|
||||||
|
assert_eq!(clauses.0[0],
|
||||||
|
ColumnConstraint::Equals(QualifiedAlias("transactions00".to_string(), Column::Transactions(TransactionsColumn::Tx)),
|
||||||
|
QueryValue::TypedValue(TypedValue::Ref(1000))).into());
|
||||||
|
|
||||||
|
let bindings = cc.column_bindings;
|
||||||
|
assert_eq!(bindings.len(), 5);
|
||||||
|
|
||||||
|
assert_eq!(bindings.get(&Variable::from_valid_name("?e")).expect("column binding for ?e").clone(),
|
||||||
|
vec![QualifiedAlias("transactions00".to_string(), Column::Transactions(TransactionsColumn::Entity))]);
|
||||||
|
|
||||||
|
assert_eq!(bindings.get(&Variable::from_valid_name("?a")).expect("column binding for ?a").clone(),
|
||||||
|
vec![QualifiedAlias("transactions00".to_string(), Column::Transactions(TransactionsColumn::Attribute))]);
|
||||||
|
|
||||||
|
assert_eq!(bindings.get(&Variable::from_valid_name("?v")).expect("column binding for ?v").clone(),
|
||||||
|
vec![QualifiedAlias("transactions00".to_string(), Column::Transactions(TransactionsColumn::Value))]);
|
||||||
|
|
||||||
|
assert_eq!(bindings.get(&Variable::from_valid_name("?tx")).expect("column binding for ?tx").clone(),
|
||||||
|
vec![QualifiedAlias("transactions00".to_string(), Column::Transactions(TransactionsColumn::Tx))]);
|
||||||
|
|
||||||
|
assert_eq!(bindings.get(&Variable::from_valid_name("?added")).expect("column binding for ?added").clone(),
|
||||||
|
vec![QualifiedAlias("transactions00".to_string(), Column::Transactions(TransactionsColumn::Added))]);
|
||||||
|
|
||||||
|
let known_types = cc.known_types;
|
||||||
|
assert_eq!(known_types.len(), 4);
|
||||||
|
|
||||||
|
assert_eq!(known_types.get(&Variable::from_valid_name("?e")).expect("known types for ?e").clone(),
|
||||||
|
vec![ValueType::Ref].into_iter().collect());
|
||||||
|
|
||||||
|
assert_eq!(known_types.get(&Variable::from_valid_name("?a")).expect("known types for ?a").clone(),
|
||||||
|
vec![ValueType::Ref].into_iter().collect());
|
||||||
|
|
||||||
|
assert_eq!(known_types.get(&Variable::from_valid_name("?tx")).expect("known types for ?tx").clone(),
|
||||||
|
vec![ValueType::Ref].into_iter().collect());
|
||||||
|
|
||||||
|
assert_eq!(known_types.get(&Variable::from_valid_name("?added")).expect("known types for ?added").clone(),
|
||||||
|
vec![ValueType::Boolean].into_iter().collect());
|
||||||
|
|
||||||
|
let extracted_types = cc.extracted_types;
|
||||||
|
assert_eq!(extracted_types.len(), 1);
|
||||||
|
|
||||||
|
assert_eq!(extracted_types.get(&Variable::from_valid_name("?v")).expect("extracted types for ?v").clone(),
|
||||||
|
QualifiedAlias("transactions00".to_string(), Column::Transactions(TransactionsColumn::ValueTypeTag)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,8 @@ impl ConjoiningClauses {
|
||||||
match where_fn.operator.0.as_str() {
|
match where_fn.operator.0.as_str() {
|
||||||
"fulltext" => self.apply_fulltext(known, where_fn),
|
"fulltext" => self.apply_fulltext(known, where_fn),
|
||||||
"ground" => self.apply_ground(known, where_fn),
|
"ground" => self.apply_ground(known, where_fn),
|
||||||
|
"tx-data" => self.apply_tx_data(known, where_fn),
|
||||||
|
"tx-ids" => self.apply_tx_ids(known, where_fn),
|
||||||
_ => bail!(ErrorKind::UnknownFunction(where_fn.operator.clone())),
|
_ => bail!(ErrorKind::UnknownFunction(where_fn.operator.clone())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,11 @@ pub enum BindingError {
|
||||||
/// than Datomic: we won't try to make sense of non-obvious (and potentially erroneous) bindings.
|
/// than Datomic: we won't try to make sense of non-obvious (and potentially erroneous) bindings.
|
||||||
ExpectedBindRel,
|
ExpectedBindRel,
|
||||||
|
|
||||||
|
/// Expected `[[?x ?y]]` or `[?x ...]` but got some other type of binding. Mentat is
|
||||||
|
/// deliberately more strict than Datomic: we won't try to make sense of non-obvious (and
|
||||||
|
/// potentially erroneous) bindings.
|
||||||
|
ExpectedBindRelOrBindColl,
|
||||||
|
|
||||||
/// Expected `[?x1 … ?xN]` or `[[?x1 … ?xN]]` but got some other number of bindings. Mentat is
|
/// Expected `[?x1 … ?xN]` or `[[?x1 … ?xN]]` but got some other number of bindings. Mentat is
|
||||||
/// deliberately more strict than Datomic: we prefer placeholders to omission.
|
/// deliberately more strict than Datomic: we prefer placeholders to omission.
|
||||||
InvalidNumberOfBindings { number: usize, expected: usize },
|
InvalidNumberOfBindings { number: usize, expected: usize },
|
||||||
|
|
|
@ -41,6 +41,7 @@ pub enum DatomsTable {
|
||||||
FulltextDatoms, // The fulltext-datoms view.
|
FulltextDatoms, // The fulltext-datoms view.
|
||||||
AllDatoms, // Fulltext and non-fulltext datoms.
|
AllDatoms, // Fulltext and non-fulltext datoms.
|
||||||
Computed(usize), // A computed table, tracked elsewhere in the query.
|
Computed(usize), // A computed table, tracked elsewhere in the query.
|
||||||
|
Transactions, // The transactions table, which makes the tx-data log API efficient.
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A source of rows that isn't a named table -- typically a subquery or union.
|
/// A source of rows that isn't a named table -- typically a subquery or union.
|
||||||
|
@ -66,6 +67,7 @@ impl DatomsTable {
|
||||||
DatomsTable::FulltextDatoms => "fulltext_datoms",
|
DatomsTable::FulltextDatoms => "fulltext_datoms",
|
||||||
DatomsTable::AllDatoms => "all_datoms",
|
DatomsTable::AllDatoms => "all_datoms",
|
||||||
DatomsTable::Computed(_) => "c",
|
DatomsTable::Computed(_) => "c",
|
||||||
|
DatomsTable::Transactions => "transactions",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,6 +93,17 @@ pub enum FulltextColumn {
|
||||||
Text,
|
Text,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One of the named columns of our transactions table.
|
||||||
|
#[derive(PartialEq, Eq, Clone)]
|
||||||
|
pub enum TransactionsColumn {
|
||||||
|
Entity,
|
||||||
|
Attribute,
|
||||||
|
Value,
|
||||||
|
Tx,
|
||||||
|
Added,
|
||||||
|
ValueTypeTag,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone)]
|
#[derive(PartialEq, Eq, Clone)]
|
||||||
pub enum VariableColumn {
|
pub enum VariableColumn {
|
||||||
Variable(Variable),
|
Variable(Variable),
|
||||||
|
@ -102,6 +115,7 @@ pub enum Column {
|
||||||
Fixed(DatomsColumn),
|
Fixed(DatomsColumn),
|
||||||
Fulltext(FulltextColumn),
|
Fulltext(FulltextColumn),
|
||||||
Variable(VariableColumn),
|
Variable(VariableColumn),
|
||||||
|
Transactions(TransactionsColumn),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DatomsColumn> for Column {
|
impl From<DatomsColumn> for Column {
|
||||||
|
@ -116,6 +130,12 @@ impl From<VariableColumn> for Column {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<TransactionsColumn> for Column {
|
||||||
|
fn from(from: TransactionsColumn) -> Column {
|
||||||
|
Column::Transactions(from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DatomsColumn {
|
impl DatomsColumn {
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub fn as_str(&self) -> &'static str {
|
||||||
use self::DatomsColumn::*;
|
use self::DatomsColumn::*;
|
||||||
|
@ -176,6 +196,7 @@ impl Debug for Column {
|
||||||
&Column::Fixed(ref c) => c.fmt(f),
|
&Column::Fixed(ref c) => c.fmt(f),
|
||||||
&Column::Fulltext(ref c) => c.fmt(f),
|
&Column::Fulltext(ref c) => c.fmt(f),
|
||||||
&Column::Variable(ref v) => v.fmt(f),
|
&Column::Variable(ref v) => v.fmt(f),
|
||||||
|
&Column::Transactions(ref t) => t.fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,6 +223,40 @@ impl Debug for FulltextColumn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TransactionsColumn {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
use self::TransactionsColumn::*;
|
||||||
|
match *self {
|
||||||
|
Entity => "e",
|
||||||
|
Attribute => "a",
|
||||||
|
Value => "v",
|
||||||
|
Tx => "tx",
|
||||||
|
Added => "added",
|
||||||
|
ValueTypeTag => "value_type_tag",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn associated_type_tag_column(&self) -> Option<TransactionsColumn> {
|
||||||
|
use self::TransactionsColumn::*;
|
||||||
|
match *self {
|
||||||
|
Value => Some(ValueTypeTag),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColumnName for TransactionsColumn {
|
||||||
|
fn column_name(&self) -> String {
|
||||||
|
self.as_str().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for TransactionsColumn {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
write!(f, "{}", self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A specific instance of a table within a query. E.g., "datoms123".
|
/// A specific instance of a table within a query. E.g., "datoms123".
|
||||||
pub type TableAlias = String;
|
pub type TableAlias = String;
|
||||||
|
|
||||||
|
@ -235,6 +290,7 @@ impl QualifiedAlias {
|
||||||
Column::Fixed(ref c) => c.associated_type_tag_column().map(Column::Fixed),
|
Column::Fixed(ref c) => c.associated_type_tag_column().map(Column::Fixed),
|
||||||
Column::Fulltext(_) => None,
|
Column::Fulltext(_) => None,
|
||||||
Column::Variable(_) => None,
|
Column::Variable(_) => None,
|
||||||
|
Column::Transactions(ref c) => c.associated_type_tag_column().map(Column::Transactions),
|
||||||
}.map(|d| QualifiedAlias(self.0.clone(), d))
|
}.map(|d| QualifiedAlias(self.0.clone(), d))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,6 +243,10 @@ fn push_column(qb: &mut QueryBuilder, col: &Column) -> BuildQueryResult {
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
&Column::Variable(ref vc) => push_variable_column(qb, vc),
|
&Column::Variable(ref vc) => push_variable_column(qb, vc),
|
||||||
|
&Column::Transactions(ref d) => {
|
||||||
|
qb.push_sql(d.as_str());
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1134,3 +1134,103 @@ fn test_tx_before_and_after() {
|
||||||
WHERE `datoms00`.tx < 12345");
|
WHERE `datoms00`.tx < 12345");
|
||||||
assert_eq!(args, vec![]);
|
assert_eq!(args, vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tx_ids() {
|
||||||
|
let mut schema = prepopulated_typed_schema(ValueType::Double);
|
||||||
|
associate_ident(&mut schema, NamespacedKeyword::new("db", "txInstant"), 101);
|
||||||
|
add_attribute(&mut schema, 101, Attribute {
|
||||||
|
value_type: ValueType::Instant,
|
||||||
|
multival: false,
|
||||||
|
index: true,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let query = r#"[:find ?tx :where [(tx-ids $ 1000 2000) [[?tx]]]]"#;
|
||||||
|
let SQLQuery { sql, args } = translate(&schema, query);
|
||||||
|
assert_eq!(sql, "SELECT DISTINCT `transactions00`.tx AS `?tx` \
|
||||||
|
FROM `transactions` AS `transactions00` \
|
||||||
|
WHERE 1000 <= `transactions00`.tx \
|
||||||
|
AND `transactions00`.tx < 2000");
|
||||||
|
assert_eq!(args, vec![]);
|
||||||
|
|
||||||
|
// This is rather artificial but verifies that binding the arguments to (tx-ids) works.
|
||||||
|
let query = r#"[:find ?tx :where [?first :db/txInstant #inst "2016-01-01T11:00:00.000Z"] [?last :db/txInstant #inst "2017-01-01T11:00:00.000Z"] [(tx-ids $ ?first ?last) [?tx ...]]]"#;
|
||||||
|
let SQLQuery { sql, args } = translate(&schema, query);
|
||||||
|
assert_eq!(sql, "SELECT DISTINCT `transactions02`.tx AS `?tx` \
|
||||||
|
FROM `datoms` AS `datoms00`, \
|
||||||
|
`datoms` AS `datoms01`, \
|
||||||
|
`transactions` AS `transactions02` \
|
||||||
|
WHERE `datoms00`.a = 101 \
|
||||||
|
AND `datoms00`.v = 1451646000000000 \
|
||||||
|
AND `datoms01`.a = 101 \
|
||||||
|
AND `datoms01`.v = 1483268400000000 \
|
||||||
|
AND `datoms00`.e <= `transactions02`.tx \
|
||||||
|
AND `transactions02`.tx < `datoms01`.e");
|
||||||
|
assert_eq!(args, vec![]);
|
||||||
|
|
||||||
|
// In practice the following query would be inefficient because of the filter on all_datoms.tx,
|
||||||
|
// but that is what (tx-data) is for.
|
||||||
|
let query = r#"[:find ?e ?a ?v ?tx :where [(tx-ids $ 1000 2000) [[?tx]]] [?e ?a ?v ?tx]]"#;
|
||||||
|
let SQLQuery { sql, args } = translate(&schema, query);
|
||||||
|
assert_eq!(sql, "SELECT DISTINCT `all_datoms01`.e AS `?e`, \
|
||||||
|
`all_datoms01`.a AS `?a`, \
|
||||||
|
`all_datoms01`.v AS `?v`, \
|
||||||
|
`all_datoms01`.value_type_tag AS `?v_value_type_tag`, \
|
||||||
|
`transactions00`.tx AS `?tx` \
|
||||||
|
FROM `transactions` AS `transactions00`, \
|
||||||
|
`all_datoms` AS `all_datoms01` \
|
||||||
|
WHERE 1000 <= `transactions00`.tx \
|
||||||
|
AND `transactions00`.tx < 2000 \
|
||||||
|
AND `transactions00`.tx = `all_datoms01`.tx");
|
||||||
|
assert_eq!(args, vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tx_data() {
|
||||||
|
let schema = prepopulated_typed_schema(ValueType::Double);
|
||||||
|
|
||||||
|
let query = r#"[:find ?e ?a ?v ?tx ?added :where [(tx-data $ 1000) [[?e ?a ?v ?tx ?added]]]]"#;
|
||||||
|
let SQLQuery { sql, args } = translate(&schema, query);
|
||||||
|
assert_eq!(sql, "SELECT DISTINCT `transactions00`.e AS `?e`, \
|
||||||
|
`transactions00`.a AS `?a`, \
|
||||||
|
`transactions00`.v AS `?v`, \
|
||||||
|
`transactions00`.value_type_tag AS `?v_value_type_tag`, \
|
||||||
|
`transactions00`.tx AS `?tx`, \
|
||||||
|
`transactions00`.added AS `?added` \
|
||||||
|
FROM `transactions` AS `transactions00` \
|
||||||
|
WHERE `transactions00`.tx = 1000");
|
||||||
|
assert_eq!(args, vec![]);
|
||||||
|
|
||||||
|
// Ensure that we don't project columns that we don't need, even if they are bound to named
|
||||||
|
// variables or to placeholders.
|
||||||
|
let query = r#"[:find [?a ?v ?added] :where [(tx-data $ 1000) [[?e ?a ?v _ ?added]]]]"#;
|
||||||
|
let SQLQuery { sql, args } = translate(&schema, query);
|
||||||
|
assert_eq!(sql, "SELECT `transactions00`.a AS `?a`, \
|
||||||
|
`transactions00`.v AS `?v`, \
|
||||||
|
`transactions00`.value_type_tag AS `?v_value_type_tag`, \
|
||||||
|
`transactions00`.added AS `?added` \
|
||||||
|
FROM `transactions` AS `transactions00` \
|
||||||
|
WHERE `transactions00`.tx = 1000 \
|
||||||
|
LIMIT 1");
|
||||||
|
assert_eq!(args, vec![]);
|
||||||
|
|
||||||
|
// This is awkward since the transactions table is queried twice, once to list transaction IDs
|
||||||
|
// and a second time to extract data. https://github.com/mozilla/mentat/issues/644 tracks
|
||||||
|
// improving this, perhaps by optimizing certain combinations of functions and bindings.
|
||||||
|
let query = r#"[:find ?e ?a ?v ?tx ?added :where [(tx-ids $ 1000 2000) [[?tx]]] [(tx-data $ ?tx) [[?e ?a ?v _ ?added]]]]"#;
|
||||||
|
let SQLQuery { sql, args } = translate(&schema, query);
|
||||||
|
assert_eq!(sql, "SELECT DISTINCT `transactions01`.e AS `?e`, \
|
||||||
|
`transactions01`.a AS `?a`, \
|
||||||
|
`transactions01`.v AS `?v`, \
|
||||||
|
`transactions01`.value_type_tag AS `?v_value_type_tag`, \
|
||||||
|
`transactions00`.tx AS `?tx`, \
|
||||||
|
`transactions01`.added AS `?added` \
|
||||||
|
FROM `transactions` AS `transactions00`, \
|
||||||
|
`transactions` AS `transactions01` \
|
||||||
|
WHERE 1000 <= `transactions00`.tx \
|
||||||
|
AND `transactions00`.tx < 2000 \
|
||||||
|
AND `transactions01`.tx = `transactions00`.tx");
|
||||||
|
assert_eq!(args, vec![]);
|
||||||
|
}
|
||||||
|
|
114
tests/query.rs
114
tests/query.rs
|
@ -27,6 +27,7 @@ use chrono::FixedOffset;
|
||||||
|
|
||||||
use mentat_core::{
|
use mentat_core::{
|
||||||
DateTime,
|
DateTime,
|
||||||
|
Entid,
|
||||||
HasSchema,
|
HasSchema,
|
||||||
KnownEntid,
|
KnownEntid,
|
||||||
TypedValue,
|
TypedValue,
|
||||||
|
@ -48,6 +49,7 @@ use mentat::{
|
||||||
Queryable,
|
Queryable,
|
||||||
QueryResults,
|
QueryResults,
|
||||||
Store,
|
Store,
|
||||||
|
TxReport,
|
||||||
Variable,
|
Variable,
|
||||||
new_connection,
|
new_connection,
|
||||||
};
|
};
|
||||||
|
@ -1339,3 +1341,115 @@ fn test_aggregation_implicit_grouping() {
|
||||||
x => panic!("Got unexpected results {:?}", x),
|
x => panic!("Got unexpected results {:?}", x),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tx_ids() {
|
||||||
|
let mut store = Store::open("").expect("opened");
|
||||||
|
|
||||||
|
store.transact(r#"[
|
||||||
|
[:db/add "a" :db/ident :foo/term]
|
||||||
|
[:db/add "a" :db/valueType :db.type/string]
|
||||||
|
[:db/add "a" :db/fulltext false]
|
||||||
|
[:db/add "a" :db/cardinality :db.cardinality/many]
|
||||||
|
]"#).unwrap();
|
||||||
|
|
||||||
|
let tx1 = store.transact(r#"[
|
||||||
|
[:db/add "v" :foo/term "1"]
|
||||||
|
]"#).expect("tx1 to apply").tx_id;
|
||||||
|
|
||||||
|
let tx2 = store.transact(r#"[
|
||||||
|
[:db/add "v" :foo/term "2"]
|
||||||
|
]"#).expect("tx2 to apply").tx_id;
|
||||||
|
|
||||||
|
let tx3 = store.transact(r#"[
|
||||||
|
[:db/add "v" :foo/term "3"]
|
||||||
|
]"#).expect("tx3 to apply").tx_id;
|
||||||
|
|
||||||
|
fn assert_tx_id_range(store: &Store, after: Entid, before: Entid, expected: Vec<TypedValue>) {
|
||||||
|
// TODO: after https://github.com/mozilla/mentat/issues/641, use q_prepare with inputs bound
|
||||||
|
// at execution time.
|
||||||
|
let r = store.q_once(r#"[:find [?tx ...]
|
||||||
|
:in ?after ?before
|
||||||
|
:where
|
||||||
|
[(tx-ids $ ?after ?before) [?tx ...]]
|
||||||
|
]"#,
|
||||||
|
QueryInputs::with_value_sequence(vec![
|
||||||
|
(Variable::from_valid_name("?after"), TypedValue::Ref(after)),
|
||||||
|
(Variable::from_valid_name("?before"), TypedValue::Ref(before)),
|
||||||
|
]))
|
||||||
|
.expect("results")
|
||||||
|
.into();
|
||||||
|
match r {
|
||||||
|
QueryResults::Coll(txs) => {
|
||||||
|
assert_eq!(txs, expected);
|
||||||
|
},
|
||||||
|
x => panic!("Got unexpected results {:?}", x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_tx_id_range(&store, tx1, tx2, vec![TypedValue::Ref(tx1)]);
|
||||||
|
assert_tx_id_range(&store, tx1, tx3, vec![TypedValue::Ref(tx1), TypedValue::Ref(tx2)]);
|
||||||
|
assert_tx_id_range(&store, tx2, tx3, vec![TypedValue::Ref(tx2)]);
|
||||||
|
assert_tx_id_range(&store, tx2, tx3 + 1, vec![TypedValue::Ref(tx2), TypedValue::Ref(tx3)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tx_data() {
|
||||||
|
let mut store = Store::open("").expect("opened");
|
||||||
|
|
||||||
|
store.transact(r#"[
|
||||||
|
[:db/add "a" :db/ident :foo/term]
|
||||||
|
[:db/add "a" :db/valueType :db.type/string]
|
||||||
|
[:db/add "a" :db/fulltext false]
|
||||||
|
[:db/add "a" :db/cardinality :db.cardinality/many]
|
||||||
|
]"#).unwrap();
|
||||||
|
|
||||||
|
let tx1 = store.transact(r#"[
|
||||||
|
[:db/add "e" :foo/term "1"]
|
||||||
|
]"#).expect("tx1 to apply");
|
||||||
|
|
||||||
|
let tx2 = store.transact(r#"[
|
||||||
|
[:db/add "e" :foo/term "2"]
|
||||||
|
]"#).expect("tx2 to apply");
|
||||||
|
|
||||||
|
fn assert_tx_data(store: &Store, tx: &TxReport, value: TypedValue) {
|
||||||
|
// TODO: after https://github.com/mozilla/mentat/issues/641, use q_prepare with inputs bound
|
||||||
|
// at execution time.
|
||||||
|
let r = store.q_once(r#"[:find ?e ?a-name ?v ?tx ?added
|
||||||
|
:in ?tx-in
|
||||||
|
:where
|
||||||
|
[(tx-data $ ?tx-in) [[?e ?a ?v ?tx ?added]]]
|
||||||
|
[?a :db/ident ?a-name]
|
||||||
|
:order ?e
|
||||||
|
]"#,
|
||||||
|
QueryInputs::with_value_sequence(vec![
|
||||||
|
(Variable::from_valid_name("?tx-in"), TypedValue::Ref(tx.tx_id)),
|
||||||
|
]))
|
||||||
|
.expect("results")
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let e = tx.tempids.get("e").cloned().expect("tempid");
|
||||||
|
|
||||||
|
match r {
|
||||||
|
QueryResults::Rel(vals) => {
|
||||||
|
assert_eq!(vals,
|
||||||
|
vec![
|
||||||
|
vec![TypedValue::Ref(e),
|
||||||
|
TypedValue::typed_ns_keyword("foo", "term"),
|
||||||
|
value,
|
||||||
|
TypedValue::Ref(tx.tx_id),
|
||||||
|
TypedValue::Boolean(true)],
|
||||||
|
vec![TypedValue::Ref(tx.tx_id),
|
||||||
|
TypedValue::typed_ns_keyword("db", "txInstant"),
|
||||||
|
TypedValue::Instant(tx.tx_instant),
|
||||||
|
TypedValue::Ref(tx.tx_id),
|
||||||
|
TypedValue::Boolean(true)],
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
x => panic!("Got unexpected results {:?}", x),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_tx_data(&store, &tx1, TypedValue::String("1".to_string().into()));
|
||||||
|
assert_tx_data(&store, &tx2, TypedValue::String("2".to_string().into()));
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue