Skip to main content

schema_core/config/
field.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::common;
6
7use super::{Aggregate, AggregateKey, Filter, FlussoType, Join, JoinKind, Through, Transform};
8
9/// One field of a document: a name, optional OpenSearch mapping `options` passed
10/// through to the index, and a [`source`](Self::source) saying where its value
11/// comes from. A leaf field's *type* is declared on its source (a
12/// [`Column`]'s [`ty`](Column::ty), an [`Aggregate`]'s
13/// [`value_type`](Aggregate::value_type)) so the document shape is known without
14/// a database.
15#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
16pub struct Field {
17    pub field: common::FieldName,
18    /// Extra OpenSearch mapping properties merged beside the derived `type`
19    /// (e.g. `analyzer`, `scaling_factor`). Empty for most fields.
20    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
21    pub options: BTreeMap<String, common::GenericValue>,
22    pub source: FieldSource,
23}
24
25/// Where a field's value comes from. The shapes are mutually exclusive — a field
26/// is exactly one of them — which is why this is an enum rather than a bag of
27/// optional `column` / `relation` / `fields` that can contradict each other.
28#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum FieldSource {
31    /// A column of the current row, optionally transformed, with an optional
32    /// fallback when the column is null.
33    Column(Column),
34    /// A sub-object assembled from sibling fields of the *same* row (it adds a
35    /// nesting level in the document without reading a related table).
36    Group(Vec<Field>),
37    /// A geographic point assembled from two same-row columns
38    /// ([`lat`](Geo::lat)/[`lon`](Geo::lon)) into an OpenSearch `geo_point`.
39    Geo(Geo),
40    /// Data drawn from a related table — folded in as nested documents
41    /// ([`Join`](Relation::Join)) or reduced to a single value
42    /// ([`Aggregate`](Relation::Aggregate)).
43    Relation(Relation),
44    /// A constant value with no database source — `None` renders as null.
45    Constant(common::GenericValue),
46}
47
48/// A column-backed field: the column to read, its declared type and nullability,
49/// the transforms to apply, and a default to coalesce nulls to.
50#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
51pub struct Column {
52    pub column: common::ColumnName,
53    /// The declared type — the OpenSearch mapping derives from it, and a live
54    /// database (when reachable) is checked against it.
55    pub ty: FlussoType,
56    /// Whether the column admits null. The resolver still forces non-null for a
57    /// primary key or a column with a `default`.
58    pub nullable: bool,
59    #[serde(default, skip_serializing_if = "Vec::is_empty")]
60    pub transforms: Vec<Transform>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub default: Option<common::GenericValue>,
63}
64
65/// A geographic point built from two same-row columns. Resolves to an
66/// OpenSearch `geo_point`; the document carries `{ "lat": …, "lon": … }`, or
67/// SQL `NULL` when either column is null (so a nullable point is absent rather
68/// than `{lat: null, lon: null}`, which OpenSearch would reject).
69#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
70pub struct Geo {
71    /// The latitude column (degrees).
72    pub lat: common::ColumnName,
73    /// The longitude column (degrees).
74    pub lon: common::ColumnName,
75    /// Whether the point may be absent — true unless the field is `required`.
76    pub nullable: bool,
77}
78
79/// How a field draws on a related table: either folding its rows in as nested
80/// documents ([`Join`](Self::Join)) or reducing them to a single value
81/// ([`Aggregate`](Self::Aggregate)).
82#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum Relation {
85    /// Fold the related rows in as nested documents, projecting `fields` from
86    /// each one.
87    Join(Join),
88    /// Reduce the related rows to a single scalar.
89    Aggregate(Aggregate),
90}
91
92impl Field {
93    /// The fields nested directly under this one: a [`Group`](FieldSource::Group)'s
94    /// members or a [`Join`](Relation::Join)'s projection. Columns, aggregates,
95    /// and constants have none.
96    pub fn children(&self) -> &[Field] {
97        match &self.source {
98            FieldSource::Group(fields) => fields,
99            FieldSource::Relation(Relation::Join(join)) => &join.fields,
100            FieldSource::Column(_)
101            | FieldSource::Geo(_)
102            | FieldSource::Relation(Relation::Aggregate(_))
103            | FieldSource::Constant(_) => &[],
104        }
105    }
106
107    /// The relation this field draws on, if it draws on a related table.
108    pub fn relation(&self) -> Option<&Relation> {
109        match &self.source {
110            FieldSource::Relation(relation) => Some(relation),
111            _ => None,
112        }
113    }
114
115    /// The column this field reads, if it is a plain column field.
116    pub fn column(&self) -> Option<&common::ColumnName> {
117        match &self.source {
118            FieldSource::Column(column) => Some(&column.column),
119            _ => None,
120        }
121    }
122}
123
124/// A relation's key, viewed uniformly across joins and aggregates — the three
125/// physical shapes a "these tables connect" fact can take. Traversal code
126/// (document SQL, reverse resolution) matches on this instead of caring whether
127/// the relation is a join or an aggregate.
128#[derive(Debug, Clone, Copy)]
129pub enum RelationKey<'a> {
130    /// The **parent** row holds the key: `parent.column → target.primary_key`
131    /// (a `belongs_to`).
132    Local(&'a common::ColumnName),
133    /// The **related** rows hold the key: `target.foreign_key → parent.pk`
134    /// (a `has_one`/`has_many`, or a direct-keyed aggregate).
135    Direct(&'a common::ColumnName),
136    /// Both sides connect through a junction table.
137    Through(&'a Through),
138}
139
140impl Relation {
141    /// The related table this relation targets.
142    pub fn table(&self) -> &common::TableName {
143        match self {
144            Relation::Join(join) => &join.table,
145            Relation::Aggregate(aggregate) => &aggregate.table,
146        }
147    }
148
149    /// The key tying the related rows and the parent row together.
150    pub fn key(&self) -> RelationKey<'_> {
151        match self {
152            Relation::Join(join) => match &join.kind {
153                JoinKind::BelongsTo { column } => RelationKey::Local(column),
154                JoinKind::HasOne { foreign_key } | JoinKind::HasMany { foreign_key } => {
155                    RelationKey::Direct(foreign_key)
156                }
157                JoinKind::ManyToMany { through } => RelationKey::Through(through),
158            },
159            Relation::Aggregate(aggregate) => match &aggregate.key {
160                AggregateKey::Direct(foreign_key) => RelationKey::Direct(foreign_key),
161                AggregateKey::Through(through) => RelationKey::Through(through),
162            },
163        }
164    }
165
166    /// Filters narrowing the related rows, if any.
167    pub fn filters(&self) -> Option<&[Filter]> {
168        match self {
169            Relation::Join(join) => join.filters.as_deref(),
170            Relation::Aggregate(aggregate) => aggregate.filters.as_deref(),
171        }
172    }
173}
174
175/// OpenSearch mapping. `mapping_type` is required; all other properties are passed through as-is.
176#[derive(Debug, Clone, Hash)]
177pub struct Mapping {
178    pub mapping_type: MappingType,
179    pub extra: BTreeMap<String, common::GenericValue>,
180}
181
182/// Serializes the way OpenSearch expects a field mapping — `{ "type": …, …extra }`
183/// — rather than the struct's two named fields. The `extra` settings sit beside
184/// `type`, exactly as they would in the index body.
185impl Serialize for Mapping {
186    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
187        use serde::ser::SerializeMap;
188        let mut map = serializer.serialize_map(Some(1 + self.extra.len()))?;
189        map.serialize_entry("type", &self.mapping_type)?;
190        for (key, value) in &self.extra {
191            map.serialize_entry(key, value)?;
192        }
193        map.end()
194    }
195}
196
197#[derive(Debug, Clone, Hash, PartialEq, Eq)]
198pub enum MappingType {
199    Text,
200    Keyword,
201    Boolean,
202    Byte,
203    Short,
204    Integer,
205    Long,
206    Float,
207    Double,
208    HalfFloat,
209    ScaledFloat,
210    Date,
211    Object,
212    Nested,
213    /// Any mapping type not covered above.
214    Other(String),
215}
216
217impl MappingType {
218    /// The OpenSearch type name (`keyword`, `half_float`, …). An [`Other`] type
219    /// is its own verbatim name.
220    ///
221    /// [`Other`]: MappingType::Other
222    pub fn name(&self) -> &str {
223        match self {
224            MappingType::Text => "text",
225            MappingType::Keyword => "keyword",
226            MappingType::Boolean => "boolean",
227            MappingType::Byte => "byte",
228            MappingType::Short => "short",
229            MappingType::Integer => "integer",
230            MappingType::Long => "long",
231            MappingType::Float => "float",
232            MappingType::Double => "double",
233            MappingType::HalfFloat => "half_float",
234            MappingType::ScaledFloat => "scaled_float",
235            MappingType::Date => "date",
236            MappingType::Object => "object",
237            MappingType::Nested => "nested",
238            MappingType::Other(name) => name,
239        }
240    }
241
242    /// The mapping type for an OpenSearch type name — the inverse of
243    /// [`name`](Self::name). An unrecognized name becomes [`Other`].
244    ///
245    /// [`Other`]: MappingType::Other
246    pub fn from_name(name: &str) -> MappingType {
247        match name {
248            "text" => MappingType::Text,
249            "keyword" => MappingType::Keyword,
250            "boolean" => MappingType::Boolean,
251            "byte" => MappingType::Byte,
252            "short" => MappingType::Short,
253            "integer" => MappingType::Integer,
254            "long" => MappingType::Long,
255            "float" => MappingType::Float,
256            "double" => MappingType::Double,
257            "half_float" => MappingType::HalfFloat,
258            "scaled_float" => MappingType::ScaledFloat,
259            "date" => MappingType::Date,
260            "object" => MappingType::Object,
261            "nested" => MappingType::Nested,
262            other => MappingType::Other(other.to_owned()),
263        }
264    }
265}
266
267/// Serializes as the bare type name (`"keyword"`).
268/// Used instead of serde with inner at other because this code will not fail
269/// So having the name function will keep a single point of failure.
270impl Serialize for MappingType {
271    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
272        serializer.serialize_str(self.name())
273    }
274}