Skip to main content

schema_core/config/
projection.rs

1//! Projecting a self-describing schema into a fully-typed mapping — without a
2//! database.
3//!
4//! Every gap a thin config once left to the source is now stated in the schema:
5//! a column field carries its [`FlussoType`](super::FlussoType) and nullability,
6//! an aggregate its result type. So the mapping follows from the schema alone.
7//! The structural rules are unchanged from when the source derived them — a
8//! group is an `object`, a to-many join is a `nested` array, a `count` is a
9//! non-null `long`, a primary key is never null — they just no longer need a
10//! round-trip to ask.
11
12use crate::common::{ColumnName, GenericValue, IndexName};
13
14use super::{
15    Aggregate, AggregateOp, Column, ContentHash, Field, FieldSource, IndexMapping, IndexSchema,
16    Mapping, MappingType, Relation, ResolvedField,
17};
18
19impl IndexSchema {
20    /// Project this schema into its fully-typed [`IndexMapping`].
21    pub fn resolve(&self, index: IndexName) -> IndexMapping {
22        resolve_index(index, self)
23    }
24}
25
26fn resolve_index(index: IndexName, schema: &IndexSchema) -> IndexMapping {
27    IndexMapping {
28        index,
29        // Hash the parsed schema, not the file: structural changes (including a
30        // declared type) flip the hash; cosmetic file changes do not.
31        hash: ContentHash::of(schema),
32        fields: resolve_fields(&schema.fields, schema.primary_key.as_ref()),
33    }
34}
35
36/// Resolve a list of fields. `primary_key` is the root table's key while we are
37/// still on the root row (it passes through groups, which stay on the same row);
38/// it is `None` once we cross into a related table via a join.
39fn resolve_fields(fields: &[Field], primary_key: Option<&ColumnName>) -> Vec<ResolvedField> {
40    fields
41        .iter()
42        .map(|field| resolve_field(field, primary_key))
43        .collect()
44}
45
46fn resolve_field(field: &Field, primary_key: Option<&ColumnName>) -> ResolvedField {
47    let (child_fields, child_pk): (&[Field], Option<&ColumnName>) = match &field.source {
48        FieldSource::Relation(Relation::Join(join)) => (&join.fields, Some(&join.primary_key)),
49        FieldSource::Group(fields) => (fields, primary_key),
50        _ => (&[], primary_key),
51    };
52    let children = resolve_fields(child_fields, child_pk);
53
54    let (mapping_type, nullable, array) = type_and_nullability(field, primary_key);
55    let mapping = Mapping {
56        mapping_type,
57        extra: field.options.clone(),
58    };
59
60    ResolvedField {
61        name: field.field.clone(),
62        mapping,
63        nullable,
64        array,
65        children,
66    }
67}
68
69/// Returns `(mapping_type, nullable, array)`. `array` is true only for an `ids`
70/// aggregate, whose `mapping_type` is then the element type.
71fn type_and_nullability(
72    field: &Field,
73    primary_key: Option<&ColumnName>,
74) -> (MappingType, bool, bool) {
75    match &field.source {
76        FieldSource::Column(Column {
77            column,
78            ty,
79            nullable,
80            default,
81            ..
82        }) => {
83            let forced_non_null = primary_key == Some(column) || default.is_some();
84            (ty.opensearch(), *nullable && !forced_non_null, false)
85        }
86        FieldSource::Group(_) => (MappingType::Object, false, false),
87        FieldSource::Geo(geo) => (
88            MappingType::Other("geo_point".to_owned()),
89            geo.nullable,
90            false,
91        ),
92        FieldSource::Constant(value) => (
93            constant_mapping_type(value),
94            matches!(value, GenericValue::Null),
95            false,
96        ),
97        FieldSource::Relation(Relation::Join(join)) => {
98            if join.kind.is_to_many() {
99                (MappingType::Nested, false, false)
100            } else {
101                (MappingType::Object, true, false)
102            }
103        }
104        FieldSource::Relation(Relation::Aggregate(aggregate)) => aggregate_type(aggregate),
105    }
106}
107
108fn aggregate_type(aggregate: &Aggregate) -> (MappingType, bool, bool) {
109    match &aggregate.op {
110        AggregateOp::Count => (MappingType::Long, false, false),
111        AggregateOp::Avg(_) => (MappingType::Double, true, false),
112        AggregateOp::Sum(_) | AggregateOp::Min(_) | AggregateOp::Max(_) => {
113            let mapping_type = aggregate
114                .value_type
115                .as_ref()
116                .map(|ty| ty.opensearch())
117                // Conversion requires a `value_type` for these ops; `double` is
118                // a defensive fallback that should never be reached.
119                .unwrap_or(MappingType::Double);
120            (mapping_type, true, false)
121        }
122        AggregateOp::Ids { element_type } => (element_type.opensearch(), false, true),
123    }
124}
125
126#[cfg(test)]
127mod tests;
128
129/// The mapping type a constant value's shape implies.
130fn constant_mapping_type(value: &GenericValue) -> MappingType {
131    match value {
132        GenericValue::Bool(_) => MappingType::Boolean,
133        GenericValue::SmallInt(_) => MappingType::Short,
134        GenericValue::Int(_) => MappingType::Integer,
135        GenericValue::BigInt(_) => MappingType::Long,
136        GenericValue::Float(_) => MappingType::Float,
137        GenericValue::Double(_) | GenericValue::Decimal(_) => MappingType::Double,
138        GenericValue::Date(_)
139        | GenericValue::Time(_)
140        | GenericValue::Timestamp(_)
141        | GenericValue::TimestampTz(_) => MappingType::Date,
142        GenericValue::Bytes(_) => MappingType::Other("binary".to_owned()),
143        GenericValue::Array(items) => items
144            .first()
145            .map(constant_mapping_type)
146            .unwrap_or(MappingType::Keyword),
147        GenericValue::Map(_) => MappingType::Object,
148        GenericValue::String(_) | GenericValue::Uuid(_) | GenericValue::Null => {
149            MappingType::Keyword
150        }
151    }
152}