#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
use crate::common::{ColumnName, FieldName, IndexName, TableName};
use crate::config::{
Aggregate, AggregateKey, AggregateOp, DatabaseSchema, Field, FieldSource, FlussoType,
IndexSchema, Join, JoinKind, MappingType, Relation,
};
fn ids_schema(element_type: FlussoType) -> IndexSchema {
IndexSchema {
version: 1,
table: TableName::try_new("users").unwrap(),
db_schema: DatabaseSchema::default(),
primary_key: None,
doc_id: None,
soft_delete: None,
filters: None,
fields: vec![Field {
field: FieldName::try_new("orderIds").unwrap(),
options: Default::default(),
source: FieldSource::Relation(Relation::Aggregate(Aggregate {
table: TableName::try_new("orders").unwrap(),
op: AggregateOp::Ids { element_type },
key: AggregateKey::Direct(crate::common::ColumnName::try_new("user_id").unwrap()),
value_type: None,
filters: None,
distinct: false,
})),
}],
}
}
fn belongs_to_schema(nullable: bool) -> IndexSchema {
IndexSchema {
version: 1,
table: TableName::try_new("tickets").unwrap(),
db_schema: DatabaseSchema::default(),
primary_key: None,
doc_id: None,
soft_delete: None,
filters: None,
fields: vec![Field {
field: FieldName::try_new("author").unwrap(),
options: Default::default(),
source: FieldSource::Relation(Relation::Join(Join {
table: TableName::try_new("users").unwrap(),
kind: JoinKind::BelongsTo {
column: ColumnName::try_new("author_id").unwrap(),
},
primary_key: ColumnName::try_new("id").unwrap(),
nullable,
filters: None,
order_by: None,
limit: None,
fields: vec![],
})),
}],
}
}
#[test]
fn to_one_join_nullability_flows_into_the_mapping() {
let mapping = belongs_to_schema(true).resolve(IndexName::try_new("tickets").unwrap());
let field = &mapping.fields[0];
assert_eq!(field.mapping.mapping_type, MappingType::Object);
assert!(field.nullable, "nullable to-one join → nullable object");
let mapping = belongs_to_schema(false).resolve(IndexName::try_new("tickets").unwrap());
let field = &mapping.fields[0];
assert_eq!(field.mapping.mapping_type, MappingType::Object);
assert!(!field.nullable, "required to-one join → non-null object");
}
fn column_schema(name: &str, ty: FlussoType) -> IndexSchema {
IndexSchema {
version: 1,
table: TableName::try_new("accounts").unwrap(),
db_schema: DatabaseSchema::default(),
primary_key: None,
doc_id: None,
soft_delete: None,
filters: None,
fields: vec![Field {
field: FieldName::try_new(name).unwrap(),
options: Default::default(),
source: FieldSource::Column(crate::config::Column {
column: ColumnName::try_new(name).unwrap(),
ty,
nullable: false,
transforms: vec![],
default: None,
enum_order: Vec::new(),
}),
}],
}
}
#[test]
fn decimal_maps_to_double_but_flags_decimal() {
let m = column_schema("creditLimit", FlussoType::Decimal)
.resolve(IndexName::try_new("accounts").unwrap());
let field = &m.fields[0];
assert_eq!(field.mapping.mapping_type, MappingType::Double);
assert!(field.mapping.decimal, "a `decimal` column flags `decimal`");
let m =
column_schema("ratio", FlussoType::Double).resolve(IndexName::try_new("accounts").unwrap());
let field = &m.fields[0];
assert_eq!(field.mapping.mapping_type, MappingType::Double);
assert!(!field.mapping.decimal, "a `double` column is not a decimal");
}
fn enum_schema(name: &str, order: Vec<String>) -> IndexSchema {
IndexSchema {
version: 1,
table: TableName::try_new("accounts").unwrap(),
db_schema: DatabaseSchema::default(),
primary_key: None,
doc_id: None,
soft_delete: None,
filters: None,
fields: vec![Field {
field: FieldName::try_new(name).unwrap(),
options: Default::default(),
source: FieldSource::Column(crate::config::Column {
column: ColumnName::try_new(name).unwrap(),
ty: FlussoType::Enum,
nullable: false,
transforms: vec![],
default: None,
enum_order: order,
}),
}],
}
}
#[test]
fn enum_order_projects_onto_the_mapping_but_type_stays_keyword() {
let order = vec!["low".to_owned(), "medium".to_owned(), "high".to_owned()];
let m = enum_schema("status", order.clone()).resolve(IndexName::try_new("accounts").unwrap());
let field = &m.fields[0];
assert_eq!(field.mapping.mapping_type, MappingType::Keyword);
assert_eq!(field.mapping.enum_order.as_deref(), Some(order.as_slice()));
let m = enum_schema("kind", Vec::new()).resolve(IndexName::try_new("accounts").unwrap());
assert_eq!(m.fields[0].mapping.enum_order, None);
}
#[test]
fn ids_projects_to_a_non_null_element_typed_array() {
let mapping = ids_schema(FlussoType::Long).resolve(IndexName::try_new("users").unwrap());
let field = &mapping.fields[0];
assert_eq!(field.mapping.mapping_type, MappingType::Long);
assert!(field.array);
assert!(!field.nullable);
let mapping = ids_schema(FlussoType::Keyword).resolve(IndexName::try_new("users").unwrap());
let field = &mapping.fields[0];
assert_eq!(field.mapping.mapping_type, MappingType::Keyword);
assert!(field.array);
assert!(!field.nullable);
}
fn through() -> crate::config::Through {
crate::config::Through {
table: TableName::try_new("order_items").unwrap(),
left_key: ColumnName::try_new("order_id").unwrap(),
right_key: ColumnName::try_new("product_id").unwrap(),
filters: None,
}
}
fn through_schema() -> IndexSchema {
IndexSchema {
version: 1,
table: TableName::try_new("orders").unwrap(),
db_schema: DatabaseSchema::default(),
primary_key: None,
doc_id: None,
soft_delete: None,
filters: None,
fields: vec![
Field {
field: FieldName::try_new("totalWeight").unwrap(),
options: Default::default(),
source: FieldSource::Relation(Relation::Aggregate(Aggregate {
table: TableName::try_new("products").unwrap(),
op: AggregateOp::Sum(ColumnName::try_new("weight").unwrap()),
key: AggregateKey::Through(through()),
value_type: Some(FlussoType::Double),
filters: None,
distinct: false,
})),
},
Field {
field: FieldName::try_new("products").unwrap(),
options: Default::default(),
source: FieldSource::Relation(Relation::Join(Join {
table: TableName::try_new("products").unwrap(),
kind: JoinKind::ManyToMany { through: through() },
primary_key: ColumnName::try_new("id").unwrap(),
nullable: false,
filters: None,
order_by: None,
limit: None,
fields: vec![],
})),
},
],
}
}
#[test]
fn a_through_schema_keeps_its_content_hash() {
let mapping = through_schema().resolve(IndexName::try_new("orders").unwrap());
assert_eq!(mapping.hash.to_string(), "af5b97ca");
}
#[test]
fn an_empty_junction_filter_list_keeps_the_content_hash() {
let base = through_schema()
.resolve(IndexName::try_new("orders").unwrap())
.hash;
let mut empty = through_schema();
if let FieldSource::Relation(Relation::Aggregate(aggregate)) = &mut empty.fields[0].source
&& let AggregateKey::Through(through) = &mut aggregate.key
{
through.filters = Some(vec![]);
}
assert_eq!(
base,
empty.resolve(IndexName::try_new("orders").unwrap()).hash
);
}
#[test]
fn opting_into_distinct_or_junction_filters_changes_the_content_hash() {
let base = through_schema()
.resolve(IndexName::try_new("orders").unwrap())
.hash;
let mut distinct = through_schema();
if let FieldSource::Relation(Relation::Aggregate(aggregate)) = &mut distinct.fields[0].source {
aggregate.distinct = true;
}
let distinct = distinct.resolve(IndexName::try_new("orders").unwrap()).hash;
let mut filtered = through_schema();
if let FieldSource::Relation(Relation::Aggregate(aggregate)) = &mut filtered.fields[0].source
&& let AggregateKey::Through(through) = &mut aggregate.key
{
through.filters = Some(vec![crate::config::Filter::NullCheck(
crate::config::NullCheckFilter {
column: ColumnName::try_new("cancelled_at").unwrap(),
op: crate::config::NullOp::IsNull,
},
)]);
}
let filtered = filtered.resolve(IndexName::try_new("orders").unwrap()).hash;
assert_ne!(base, distinct);
assert_ne!(base, filtered);
assert_ne!(distinct, filtered);
}