1use crate::common::{ColumnName, GenericValue, IndexName};
13
14use super::{
15 Aggregate, AggregateOp, Column, ContentHash, Field, FieldSource, FlussoType, IndexMapping,
16 IndexSchema, 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 map_values: map_value_type(field),
59 decimal: is_decimal(field),
60 enum_order: enum_order(field),
61 };
62
63 ResolvedField {
64 name: field.field.clone(),
65 mapping,
66 nullable,
67 array,
68 children,
69 }
70}
71
72fn map_value_type(field: &Field) -> Option<MappingType> {
76 match &field.source {
77 FieldSource::Column(Column {
78 ty: FlussoType::Map { values },
79 ..
80 }) => Some(values.opensearch()),
81 _ => None,
82 }
83}
84
85fn enum_order(field: &Field) -> Option<Vec<String>> {
89 match &field.source {
90 FieldSource::Column(Column {
91 ty: FlussoType::Enum,
92 enum_order,
93 ..
94 }) if !enum_order.is_empty() => Some(enum_order.clone()),
95 _ => None,
96 }
97}
98
99fn is_decimal(field: &Field) -> bool {
104 let ty_is_decimal = |ty: &FlussoType| matches!(ty, FlussoType::Decimal);
105 match &field.source {
106 FieldSource::Column(Column { ty, .. }) => ty_is_decimal(ty),
107 FieldSource::Constant(GenericValue::Decimal(_)) => true,
108 FieldSource::Relation(Relation::Aggregate(aggregate)) => match &aggregate.op {
109 AggregateOp::Sum(_) | AggregateOp::Min(_) | AggregateOp::Max(_) => {
110 aggregate.value_type.as_ref().is_some_and(ty_is_decimal)
111 }
112 AggregateOp::Ids { element_type } => ty_is_decimal(element_type),
113 AggregateOp::Count | AggregateOp::Avg(_) => false,
114 },
115 _ => false,
116 }
117}
118
119fn type_and_nullability(
122 field: &Field,
123 primary_key: Option<&ColumnName>,
124) -> (MappingType, bool, bool) {
125 match &field.source {
126 FieldSource::Column(Column {
127 column,
128 ty,
129 nullable,
130 default,
131 ..
132 }) => {
133 let forced_non_null = primary_key == Some(column) || default.is_some();
134 (ty.opensearch(), *nullable && !forced_non_null, false)
135 }
136 FieldSource::Group(_) => (MappingType::Object, false, false),
137 FieldSource::Geo(geo) => (
138 MappingType::Other("geo_point".to_owned()),
139 geo.nullable,
140 false,
141 ),
142 FieldSource::Constant(value) => (
143 constant_mapping_type(value),
144 matches!(value, GenericValue::Null),
145 false,
146 ),
147 FieldSource::Relation(Relation::Join(join)) => {
148 let mapping_type = if join.kind.is_to_many() {
149 MappingType::Nested
150 } else {
151 MappingType::Object
152 };
153 (mapping_type, join.nullable, false)
154 }
155 FieldSource::Relation(Relation::Aggregate(aggregate)) => aggregate_type(aggregate),
156 }
157}
158
159fn aggregate_type(aggregate: &Aggregate) -> (MappingType, bool, bool) {
160 match &aggregate.op {
161 AggregateOp::Count => (MappingType::Long, false, false),
162 AggregateOp::Avg(_) => (MappingType::Double, true, false),
163 AggregateOp::Sum(_) | AggregateOp::Min(_) | AggregateOp::Max(_) => {
164 let mapping_type = aggregate
165 .value_type
166 .as_ref()
167 .map(|ty| ty.opensearch())
168 .unwrap_or(MappingType::Double);
171 (mapping_type, true, false)
172 }
173 AggregateOp::Ids { element_type } => (element_type.opensearch(), false, true),
174 }
175}
176
177#[cfg(test)]
178mod tests;
179
180fn constant_mapping_type(value: &GenericValue) -> MappingType {
182 match value {
183 GenericValue::Bool(_) => MappingType::Boolean,
184 GenericValue::SmallInt(_) => MappingType::Short,
185 GenericValue::Int(_) => MappingType::Integer,
186 GenericValue::BigInt(_) => MappingType::Long,
187 GenericValue::Float(_) => MappingType::Float,
188 GenericValue::Double(_) | GenericValue::Decimal(_) => MappingType::Double,
189 GenericValue::Date(_)
190 | GenericValue::Time(_)
191 | GenericValue::Timestamp(_)
192 | GenericValue::TimestampTz(_) => MappingType::Date,
193 GenericValue::Bytes(_) => MappingType::Other("binary".to_owned()),
194 GenericValue::Array(items) => items
195 .first()
196 .map(constant_mapping_type)
197 .unwrap_or(MappingType::Keyword),
198 GenericValue::Map(_) => MappingType::Object,
199 GenericValue::String(_) | GenericValue::Uuid(_) | GenericValue::Null => {
200 MappingType::Keyword
201 }
202 }
203}