#![cfg(feature = "mocks")]
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use ciborium::Value as CborValue;
use indexmap::IndexMap;
use vantage_core::Result;
use vantage_expressions::{
Expression,
traits::associated_expressions::AssociatedExpression,
traits::datasource::{DataSource, ExprDataSource},
traits::expressive::ExpressiveEnum,
};
use vantage_table::column::core::{Column, ColumnType};
use vantage_table::conditions::literal_equality;
use vantage_table::prelude::*;
use vantage_table::table::{BuildEnv, BuildKind, TableInjector, VistaMetadataOptions};
use vantage_types::{EmptyEntity, Entity, Record};
use vantage_vista::{JoinKey, Reference, ReferenceKind, ReferenceSpec, SpecResolver, VistaSpec};
#[derive(Clone)]
struct CborSource;
impl DataSource for CborSource {}
impl ExprDataSource<CborValue> for CborSource {
async fn execute(&self, _expr: &Expression<CborValue>) -> Result<CborValue> {
unimplemented!("the relation tests never run a query")
}
fn defer(
&self,
_expr: Expression<CborValue>,
) -> vantage_expressions::traits::expressive::DeferredFn<CborValue> {
unimplemented!("the relation tests never run a query")
}
}
fn refused<T>() -> Result<T> {
Err(vantage_core::error!("this source stores nothing"))
}
#[async_trait]
impl TableSource for CborSource {
type Column<Type>
= Column<Type>
where
Type: ColumnType;
type AnyType = CborValue;
type Value = CborValue;
type Id = String;
type Condition = Expression<CborValue>;
type Source = String;
fn condition_equality(&self, condition: &Expression<CborValue>) -> Option<(String, CborValue)> {
literal_equality(condition)
}
fn eq_value_condition(&self, field: &str, value: CborValue) -> Result<Expression<CborValue>> {
Ok(Expression::new(
"{} = {}",
vec![
ExpressiveEnum::Nested(Expression::new(field.to_string(), vec![])),
ExpressiveEnum::Nested(Expression::new("{}", vec![ExpressiveEnum::Scalar(value)])),
],
))
}
fn create_column<Type: ColumnType>(&self, name: &str) -> Column<Type> {
Column::new(name)
}
fn to_any_column<Type: ColumnType>(&self, column: Column<Type>) -> Column<CborValue> {
Column::from_column(column)
}
fn convert_any_column<Type: ColumnType>(
&self,
any_column: Column<CborValue>,
) -> Option<Column<Type>> {
Some(Column::from_column(any_column))
}
fn expr(
&self,
template: impl Into<String>,
parameters: Vec<ExpressiveEnum<CborValue>>,
) -> Expression<CborValue> {
Expression::new(template, parameters)
}
fn search_table_condition<E>(
&self,
_table: &Table<Self, E>,
_search_value: &str,
) -> Expression<CborValue>
where
E: Entity<CborValue>,
{
Expression::new("search", vec![])
}
async fn list_table_values<E>(
&self,
_table: &Table<Self, E>,
) -> Result<IndexMap<String, Record<CborValue>>>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn get_table_value<E>(
&self,
_table: &Table<Self, E>,
_id: &String,
) -> Result<Option<Record<CborValue>>>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn get_table_some_value<E>(
&self,
_table: &Table<Self, E>,
) -> Result<Option<(String, Record<CborValue>)>>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn get_table_count<E>(&self, _table: &Table<Self, E>) -> Result<i64>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn get_table_sum<E>(
&self,
_table: &Table<Self, E>,
_column: &Column<CborValue>,
) -> Result<CborValue>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn get_table_max<E>(
&self,
_table: &Table<Self, E>,
_column: &Column<CborValue>,
) -> Result<CborValue>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn get_table_min<E>(
&self,
_table: &Table<Self, E>,
_column: &Column<CborValue>,
) -> Result<CborValue>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn insert_table_value<E>(
&self,
_table: &Table<Self, E>,
_id: &String,
_record: &Record<CborValue>,
) -> Result<Record<CborValue>>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn replace_table_value<E>(
&self,
_table: &Table<Self, E>,
_id: &String,
_record: &Record<CborValue>,
) -> Result<Record<CborValue>>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn patch_table_value<E>(
&self,
_table: &Table<Self, E>,
_id: &String,
_partial: &Record<CborValue>,
) -> Result<Record<CborValue>>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn delete_table_value<E>(&self, _table: &Table<Self, E>, _id: &String) -> Result<()>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn delete_table_all_values<E>(&self, _table: &Table<Self, E>) -> Result<()>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
async fn insert_table_return_id_value<E>(
&self,
_table: &Table<Self, E>,
_record: &Record<CborValue>,
) -> Result<String>
where
E: Entity<CborValue>,
Self: Sized,
{
refused()
}
fn related_in_condition<SourceE: Entity<CborValue> + 'static>(
&self,
target_field: &str,
source_table: &Table<Self, SourceE>,
source_column: &str,
) -> Expression<CborValue>
where
Self: Sized,
{
Expression::new(
format!(
"{target_field} IN (SELECT {source_column} FROM {})",
source_table.table_name()
),
vec![],
)
}
fn related_correlated_condition(
&self,
target_table: &str,
target_field: &str,
source_table: &str,
source_column: &str,
) -> Expression<CborValue> {
Expression::new(
format!("{target_table}.{target_field} = {source_table}.{source_column}"),
vec![],
)
}
fn column_table_values_expr<'a, E, Type: ColumnType>(
&'a self,
_table: &Table<Self, E>,
_column: &Column<Type>,
) -> AssociatedExpression<'a, Self, CborValue, Vec<Type>>
where
E: Entity<CborValue> + 'static,
Self: Sized,
{
unimplemented!("the relation tests never run a query")
}
}
type CborTable = Table<CborSource, EmptyEntity>;
fn bakery(src: CborSource) -> CborTable {
Table::new("bakery", src)
.with_id_column("id")
.with_column_of::<String>("name")
}
fn order(src: CborSource) -> CborTable {
Table::new("client_order", src)
.with_id_column("id")
.with_column_of::<String>("client_id")
}
fn client() -> CborTable {
Table::new("client", CborSource)
.with_id_column("id")
.with_column_of::<String>("name")
.with_column_of::<String>("bakery_id")
.with_one("bakery", "bakery_id", bakery)
.with_many("orders", "client_id", order)
}
fn row(pairs: &[(&str, &str)]) -> Record<CborValue> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), CborValue::Text(v.to_string())))
.collect()
}
fn text(s: &str) -> CborValue {
CborValue::Text(s.to_string())
}
#[test]
fn a_table_without_relations_lists_none() {
let table: CborTable = Table::new("client", CborSource);
assert!(table.references().is_empty());
assert!(table.ref_kinds().is_empty());
assert!(table.vista_references().is_empty());
}
#[test]
fn relations_are_listed_in_declaration_order_with_their_cardinality() {
let table = client();
assert_eq!(table.references(), ["bakery", "orders"]);
assert_eq!(
table.ref_kinds(),
[
("bakery".to_string(), ReferenceKind::HasOne),
("orders".to_string(), ReferenceKind::HasMany),
]
);
assert_eq!(
table.ref_cardinality("bakery").unwrap(),
ReferenceKind::HasOne
);
assert_eq!(
table.ref_cardinality("orders").unwrap(),
ReferenceKind::HasMany
);
}
#[test]
fn a_has_ones_foreign_key_is_the_source_column_and_a_has_manys_is_the_target_column() {
let table = client();
assert_eq!(table.ref_foreign_key("bakery").unwrap(), "bakery_id");
assert_eq!(table.ref_foreign_key("orders").unwrap(), "client_id");
}
#[test]
fn asking_about_an_unknown_relation_is_an_error() {
let table = client();
assert!(table.ref_cardinality("nope").is_err());
assert!(table.ref_foreign_key("nope").is_err());
let bare: CborTable = Table::new("client", CborSource);
assert!(bare.ref_cardinality("orders").is_err());
}
#[test]
fn vista_references_name_the_target_table_and_the_join_key() {
assert_eq!(
client().vista_references(),
[
Reference::has_one("bakery", "bakery", JoinKey::to_target_id("bakery_id")),
Reference::has_many("orders", "client_order", JoinKey::new("id", "client_id")),
]
);
}
fn unbuildable(_src: CborSource) -> Result<CborTable> {
Err(vantage_core::error!("target has no spec"))
}
fn client_with_unbuildable_targets() -> CborTable {
Table::new("client", CborSource)
.with_id_column("id")
.with_column_of::<String>("bakery_id")
.try_with_one("bakery", "bakery_id", "bakery", unbuildable)
.try_with_many("orders", "client_id", "client_order", unbuildable)
}
#[test]
fn traversing_a_relation_whose_target_fails_to_build_is_an_error() {
let table = client_with_unbuildable_targets();
let alice = row(&[("id", "alice"), ("bakery_id", "b1")]);
for relation in ["bakery", "orders"] {
assert!(
table
.get_ref_from_row::<EmptyEntity>(relation, &alice)
.is_err(),
"{relation}"
);
assert!(
table.get_ref_as::<EmptyEntity>(relation).is_err(),
"{relation}"
);
assert!(table.get_ref_target_erased(relation).is_err(), "{relation}");
}
}
#[test]
fn a_relation_whose_target_fails_to_build_still_lists_under_its_declared_target() {
assert_eq!(
client_with_unbuildable_targets().vista_references(),
[
Reference::has_one("bakery", "bakery", JoinKey::to_target_id("bakery_id")),
Reference::has_many("orders", "client_order", JoinKey::new("id", "client_id")),
]
);
}
#[test]
fn vista_references_name_a_try_with_target_without_building_it() {
let builds = Arc::new(Mutex::new(0));
let counted = |builds: &Arc<Mutex<i32>>, build: fn(CborSource) -> CborTable| {
let builds = builds.clone();
move |src: CborSource| -> Result<CborTable> {
*builds.lock().unwrap() += 1;
Ok(build(src))
}
};
let table: CborTable = Table::new("client", CborSource)
.with_id_column("id")
.with_column_of::<String>("bakery_id")
.try_with_one(
"bakery",
"bakery_id",
"bakery_spec",
counted(&builds, bakery),
)
.try_with_many(
"orders",
"client_id",
"orders_spec",
counted(&builds, order),
);
assert_eq!(
table.vista_references(),
[
Reference::has_one("bakery", "bakery_spec", JoinKey::to_target_id("bakery_id")),
Reference::has_many("orders", "orders_spec", JoinKey::new("id", "client_id")),
]
);
table.vista_metadata(VistaMetadataOptions {
references: true,
..VistaMetadataOptions::default()
});
assert_eq!(*builds.lock().unwrap(), 0);
}
#[test]
fn try_with_relations_that_build_traverse_like_with_one_and_with_many() {
let table: CborTable = Table::new("client", CborSource)
.with_id_column("id")
.with_column_of::<String>("bakery_id")
.try_with_one("bakery", "bakery_id", "bakery", |src| Ok(bakery(src)))
.try_with_many("orders", "client_id", "client_order", |src| Ok(order(src)));
let alice = row(&[("id", "alice"), ("bakery_id", "b1")]);
let orders: CborTable = table.get_ref_from_row("orders", &alice).unwrap();
assert_eq!(invariant_of(&orders, "client_id"), Some(text("alice")));
let bakery: CborTable = table.get_ref_from_row("bakery", &alice).unwrap();
assert_eq!(invariant_of(&bakery, "id"), Some(text("b1")));
}
#[test]
fn a_source_that_does_not_describe_its_conditions_says_so() {
let table = client();
let condition = table
.data_source()
.eq_value_condition("name", text("a"))
.unwrap();
assert_eq!(
table.data_source().describe_condition(&condition),
"<condition>"
);
assert_eq!(table.data_source().condition_column(&condition), None);
}
#[test]
fn a_has_many_join_key_starts_from_the_tables_own_id_column() {
let table: CborTable = Table::new("client", CborSource)
.with_id_column("code")
.with_many("orders", "client_code", order);
assert_eq!(
table.vista_references(),
[Reference::has_many(
"orders",
"client_order",
JoinKey::new("code", "client_code")
)]
);
}
#[test]
fn vista_metadata_folds_in_references_only_when_asked() {
let without = client().vista_metadata(VistaMetadataOptions::default());
assert!(without.references.is_empty());
let with = client().vista_metadata(VistaMetadataOptions {
references: true,
..VistaMetadataOptions::default()
});
assert_eq!(
with.references
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
["bakery", "orders"]
);
}
#[test]
fn copy_relations_from_shares_all_or_only_the_listed_relations() {
let source = client();
let mut all: CborTable = Table::new("copy", CborSource);
all.copy_relations_from(&source, None);
assert_eq!(all.references(), ["bakery", "orders"]);
let mut some: CborTable = Table::new("copy", CborSource);
some.copy_relations_from(&source, Some(&["orders"]));
assert_eq!(some.references(), ["orders"]);
let mut none: CborTable = Table::new("copy", CborSource);
none.copy_relations_from(
&Table::<CborSource, EmptyEntity>::new("bare", CborSource),
None,
);
assert!(none.references().is_empty());
}
fn invariant_of(table: &CborTable, column: &str) -> Option<CborValue> {
table.invariants().get(column).cloned()
}
#[test]
fn a_has_many_from_a_row_reads_the_sources_id_and_narrows_the_target_by_it() {
let orders = client()
.get_ref_from_row::<EmptyEntity>("orders", &row(&[("id", "c1")]))
.unwrap();
assert_eq!(orders.table_name(), "client_order");
let conditions: Vec<_> = orders.conditions().collect();
assert_eq!(conditions.len(), 1);
assert_eq!(
literal_equality(conditions[0]),
Some(("client_id".to_string(), text("c1")))
);
assert_eq!(invariant_of(&orders, "client_id"), Some(text("c1")));
}
#[test]
fn a_has_one_from_a_row_reads_its_foreign_key_and_narrows_the_targets_id() {
let bakery = client()
.get_ref_from_row::<EmptyEntity>("bakery", &row(&[("id", "c1"), ("bakery_id", "b7")]))
.unwrap();
assert_eq!(bakery.table_name(), "bakery");
let conditions: Vec<_> = bakery.conditions().collect();
assert_eq!(conditions.len(), 1);
assert_eq!(
literal_equality(conditions[0]),
Some(("id".to_string(), text("b7")))
);
assert_eq!(invariant_of(&bakery, "id"), Some(text("b7")));
}
#[test]
fn a_row_without_the_join_field_is_an_error() {
let has_many = client().get_ref_from_row::<EmptyEntity>("orders", &row(&[("name", "x")]));
assert!(has_many.is_err());
let has_one = client().get_ref_from_row::<EmptyEntity>("bakery", &row(&[("id", "c1")]));
assert!(has_one.is_err());
}
#[test]
fn traversing_an_unknown_relation_from_a_row_is_an_error() {
assert!(
client()
.get_ref_from_row::<EmptyEntity>("nope", &row(&[("id", "c1")]))
.is_err()
);
assert!(client().get_ref_as::<EmptyEntity>("nope").is_err());
assert!(client().get_subquery_as::<EmptyEntity>("nope").is_err());
assert!(client().get_ref_target_erased("nope").is_err());
}
#[test]
fn get_ref_as_narrows_the_target_by_the_sources_set() {
let orders = client().get_ref_as::<EmptyEntity>("orders").unwrap();
assert_eq!(orders.table_name(), "client_order");
let conditions: Vec<&str> = orders.conditions().map(|c| c.template.as_str()).collect();
assert_eq!(conditions, ["client_id IN (SELECT id FROM client)"]);
}
#[test]
fn get_ref_as_from_a_has_one_matches_the_targets_id_to_the_sources_foreign_key() {
let bakery = client().get_ref_as::<EmptyEntity>("bakery").unwrap();
let conditions: Vec<&str> = bakery.conditions().map(|c| c.template.as_str()).collect();
assert_eq!(conditions, ["id IN (SELECT bakery_id FROM client)"]);
}
#[test]
fn removed_fk_column_still_traverses() {
let mut client = client();
client.remove_column("bakery_id").unwrap();
let bakery = client.get_ref_as::<EmptyEntity>("bakery").unwrap();
let conditions: Vec<&str> = bakery.conditions().map(|c| c.template.as_str()).collect();
assert_eq!(conditions, ["id IN (SELECT bakery_id FROM client)"]);
}
#[test]
fn get_subquery_as_correlates_the_target_with_the_source() {
let orders = client().get_subquery_as::<EmptyEntity>("orders").unwrap();
let conditions: Vec<&str> = orders.conditions().map(|c| c.template.as_str()).collect();
assert_eq!(conditions, ["client_order.client_id = client.id"]);
}
#[test]
fn get_subquery_erased_builds_the_same_correlated_target() {
let orders = client().get_subquery_erased("orders").unwrap();
let conditions: Vec<&str> = orders.conditions().map(|c| c.template.as_str()).collect();
assert_eq!(conditions, ["client_order.client_id = client.id"]);
assert_eq!(orders.table_name(), "client_order");
}
#[test]
fn get_ref_target_erased_is_the_bare_target_with_no_condition() {
let bakery = client().get_ref_target_erased("bakery").unwrap();
assert_eq!(bakery.table_name(), "bakery");
assert_eq!(bakery.conditions().count(), 0);
assert!(bakery.invariants().is_empty());
assert!(bakery.columns().contains_key("name"));
}
#[test]
fn with_id_narrows_to_the_id_and_registers_it_as_an_invariant() {
let one = client().with_id(CborValue::Text("c1".into())).unwrap();
let conditions: Vec<_> = one.conditions().collect();
assert_eq!(
literal_equality(conditions[0]),
Some(("id".to_string(), text("c1")))
);
assert_eq!(invariant_of(&one, "id"), Some(text("c1")));
}
type Spec = VistaSpec;
type Env = BuildEnv<Spec, CborSource>;
fn spec(name: &str, refs: &[(&str, &str)]) -> Spec {
let mut spec = Spec::new(name);
for (relation, target) in refs {
let reference: ReferenceSpec = serde_json::from_value(serde_json::json!({
"table": target,
"kind": "has_many",
"foreign_key": format!("{name}_id"),
}))
.unwrap();
spec.references.insert(relation.to_string(), reference);
}
spec
}
fn build_cbor(spec: &Spec, src: CborSource, env: &Env) -> Result<CborTable> {
let mut table = Table::new(spec.name.clone(), src)
.with_id_column("id")
.with_spec_references(spec, env, build_cbor)?;
env.inject(&spec.name, &mut table, build_cbor)?;
Ok(table)
}
fn resolver(specs: Vec<Spec>) -> Arc<dyn SpecResolver<Spec>> {
Arc::new(move |name: &str| specs.iter().find(|s| s.name == name).cloned())
}
fn tenant_condition(table: &CborTable) -> Option<CborValue> {
table
.conditions()
.find_map(literal_equality)
.filter(|(column, _)| column == "tenant")
.map(|(_, value)| value)
}
#[test]
fn injector_runs_on_reference_targets_with_kind_reference() {
let seen = Arc::new(Mutex::new(Vec::new()));
let log = seen.clone();
let injector: TableInjector<CborSource> = Arc::new(move |cx, table| {
log.lock()
.unwrap()
.push((cx.key.to_string(), cx.kind.clone()));
table.add_condition(
table
.data_source()
.eq_value_condition("tenant", text("t1"))?,
);
Ok(())
});
let specs = vec![spec("client", &[("orders", "order")]), spec("order", &[])];
let env = Env::root("client", Some(resolver(specs.clone())), Some(injector));
let clients = build_cbor(&specs[0], CborSource, &env).unwrap();
let orders = clients.get_ref_target_erased("orders").unwrap();
assert_eq!(tenant_condition(&clients), Some(text("t1")));
assert_eq!(tenant_condition(&orders), Some(text("t1")));
assert_eq!(
*seen.lock().unwrap(),
[
("client".to_string(), BuildKind::Root),
(
"order".to_string(),
BuildKind::Reference {
from: "client".into(),
relation: "orders".into()
}
),
]
);
}
#[test]
fn injector_runs_on_bare_reference_targets_without_a_resolver() {
let injector: TableInjector<CborSource> = Arc::new(|_cx, table| {
table.add_condition(
table
.data_source()
.eq_value_condition("tenant", text("t1"))?,
);
Ok(())
});
let client = spec("client", &[("orders", "order")]);
let env = Env::root("client", None, Some(injector));
let clients = build_cbor(&client, CborSource, &env).unwrap();
let orders = clients.get_ref_target_erased("orders").unwrap();
assert_eq!(orders.table_name(), "order");
assert_eq!(tenant_condition(&orders), Some(text("t1")));
}
#[test]
fn injector_builder_returns_an_injected_table() {
let parent_tenant = Arc::new(Mutex::new(None));
let seen = parent_tenant.clone();
let injector: TableInjector<CborSource> = Arc::new(move |cx, table| {
table.add_condition(
table
.data_source()
.eq_value_condition("tenant", text("t1"))?,
);
if cx.key == "order" {
let parent = (cx.build)("client")?;
*seen.lock().unwrap() = Some(tenant_condition(&parent));
}
Ok(())
});
let specs = vec![spec("client", &[]), spec("order", &[])];
let env = Env::root("order", Some(resolver(specs.clone())), Some(injector));
build_cbor(&specs[1], CborSource, &env).unwrap();
assert_eq!(*parent_tenant.lock().unwrap(), Some(Some(text("t1"))));
}
#[test]
fn injector_cycle_is_an_error_naming_the_path() {
let injector: TableInjector<CborSource> = Arc::new(|cx, _table| {
let other = if cx.key == "a" { "b" } else { "a" };
(cx.build)(other)?;
Ok(())
});
let specs = vec![spec("a", &[]), spec("b", &[])];
let env = Env::root("a", Some(resolver(specs.clone())), Some(injector));
let err = build_cbor(&specs[0], CborSource, &env).unwrap_err();
assert!(err.to_string().contains("a -> b -> a"), "{err}");
}
fn assert_orders_unreachable(clients: &CborTable, expect: &str) {
let alice = row(&[("id", "alice")]);
let errors = [
clients.get_ref_target_erased("orders").err(),
clients.get_ref_as::<EmptyEntity>("orders").err(),
clients
.get_ref_from_row::<EmptyEntity>("orders", &alice)
.err(),
];
for err in errors {
let err = err.expect("traversal must fail closed");
assert!(format!("{err:?}").contains(expect), "{err:?}");
}
}
#[test]
fn with_spec_references_fails_closed_when_the_resolver_misses_the_target() {
let specs = vec![spec("client", &[("orders", "order")])];
let env = Env::root("client", Some(resolver(specs.clone())), None);
let clients = build_cbor(&specs[0], CborSource, &env).unwrap();
assert_orders_unreachable(&clients, "reference target has no spec");
}
#[test]
fn with_spec_references_fails_closed_when_the_target_build_fails() {
let injector: TableInjector<CborSource> = Arc::new(|cx, _table| match cx.key {
"order" => Err(vantage_core::error!("orders are not for you")),
_ => Ok(()),
});
let specs = vec![spec("client", &[("orders", "order")]), spec("order", &[])];
let env = Env::root("client", Some(resolver(specs.clone())), Some(injector));
let clients = build_cbor(&specs[0], CborSource, &env).unwrap();
assert_orders_unreachable(&clients, "orders are not for you");
}
#[test]
fn a_failing_injector_fails_the_build() {
let injector: TableInjector<CborSource> =
Arc::new(|_cx, _table| Err(vantage_core::error!("not for you")));
let client = spec("client", &[]);
let env = Env::root("client", None, Some(injector));
let err = build_cbor(&client, CborSource, &env).unwrap_err();
assert!(err.to_string().contains("not for you"), "{err}");
}