1use 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 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: ContentHash::of(schema),
32 fields: resolve_fields(&schema.fields, schema.primary_key.as_ref()),
33 }
34}
35
36fn 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
69fn 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 .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 #![allow(clippy::unwrap_used, clippy::indexing_slicing)]
129
130 use crate::common::{FieldName, IndexName, TableName};
131 use crate::config::{
132 Aggregate, AggregateKey, AggregateOp, DatabaseSchema, Field, FieldSource, FlussoType,
133 IndexSchema, MappingType, Relation,
134 };
135
136 fn ids_schema(element_type: FlussoType) -> IndexSchema {
137 IndexSchema {
138 version: 1,
139 table: TableName::try_new("users").unwrap(),
140 db_schema: DatabaseSchema::default(),
141 primary_key: None,
142 doc_id: None,
143 soft_delete: None,
144 filters: None,
145 fields: vec![Field {
146 field: FieldName::try_new("orderIds").unwrap(),
147 options: Default::default(),
148 source: FieldSource::Relation(Relation::Aggregate(Aggregate {
149 table: TableName::try_new("orders").unwrap(),
150 op: AggregateOp::Ids { element_type },
151 key: AggregateKey::Direct(
152 crate::common::ColumnName::try_new("user_id").unwrap(),
153 ),
154 value_type: None,
155 filters: None,
156 })),
157 }],
158 }
159 }
160
161 #[test]
162 fn ids_projects_to_a_non_null_element_typed_array() {
163 let mapping = ids_schema(FlussoType::Long).resolve(IndexName::try_new("users").unwrap());
164 let field = &mapping.fields[0];
165 assert_eq!(field.mapping.mapping_type, MappingType::Long);
166 assert!(field.array);
167 assert!(!field.nullable);
168
169 let mapping = ids_schema(FlussoType::Keyword).resolve(IndexName::try_new("users").unwrap());
170 let field = &mapping.fields[0];
171 assert_eq!(field.mapping.mapping_type, MappingType::Keyword);
172 assert!(field.array);
173 assert!(!field.nullable);
174 }
175}
176
177fn constant_mapping_type(value: &GenericValue) -> MappingType {
179 match value {
180 GenericValue::Bool(_) => MappingType::Boolean,
181 GenericValue::Int(_) => MappingType::Long,
182 GenericValue::Decimal(_) => MappingType::Double,
183 GenericValue::Array(items) => items
184 .first()
185 .map(constant_mapping_type)
186 .unwrap_or(MappingType::Keyword),
187 GenericValue::Map(_) => MappingType::Object,
188 GenericValue::String(_) | GenericValue::Null => MappingType::Keyword,
189 }
190}