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    pub fn relation(&self) -> Option<&Relation> {
108        match &self.source {
109            FieldSource::Relation(relation) => Some(relation),
110            _ => None,
111        }
112    }
113
114    /// The column this field reads, if it is a plain column field.
115    pub fn column(&self) -> Option<&common::ColumnName> {
116        match &self.source {
117            FieldSource::Column(column) => Some(&column.column),
118            _ => None,
119        }
120    }
121}
122
123/// A relation's key, viewed uniformly across joins and aggregates — the three
124/// physical shapes a "these tables connect" fact can take. Traversal code
125/// (document SQL, reverse resolution) matches on this instead of caring whether
126/// the relation is a join or an aggregate.
127#[derive(Debug, Clone, Copy)]
128pub enum RelationKey<'a> {
129    /// The **parent** row holds the key: `parent.column → target.primary_key`
130    /// (a `belongs_to`).
131    Local(&'a common::ColumnName),
132    /// The **related** rows hold the key: `target.foreign_key → parent.pk`
133    /// (a `has_one`/`has_many`, or a direct-keyed aggregate).
134    Direct(&'a common::ColumnName),
135    /// Both sides connect through a junction table.
136    Through(&'a Through),
137}
138
139impl Relation {
140    pub fn table(&self) -> &common::TableName {
141        match self {
142            Relation::Join(join) => &join.table,
143            Relation::Aggregate(aggregate) => &aggregate.table,
144        }
145    }
146
147    /// The key tying the related rows and the parent row together.
148    pub fn key(&self) -> RelationKey<'_> {
149        match self {
150            Relation::Join(join) => match &join.kind {
151                JoinKind::BelongsTo { column } => RelationKey::Local(column),
152                JoinKind::HasOne { foreign_key } | JoinKind::HasMany { foreign_key } => {
153                    RelationKey::Direct(foreign_key)
154                }
155                JoinKind::ManyToMany { through } => RelationKey::Through(through),
156            },
157            Relation::Aggregate(aggregate) => match &aggregate.key {
158                AggregateKey::Direct(foreign_key) => RelationKey::Direct(foreign_key),
159                AggregateKey::Through(through) => RelationKey::Through(through),
160            },
161        }
162    }
163
164    /// Filters narrowing the related rows, if any.
165    pub fn filters(&self) -> Option<&[Filter]> {
166        match self {
167            Relation::Join(join) => join.filters.as_deref(),
168            Relation::Aggregate(aggregate) => aggregate.filters.as_deref(),
169        }
170    }
171}
172
173/// OpenSearch mapping. `mapping_type` is required; all other properties are passed through as-is.
174#[derive(Debug, Clone, Hash)]
175pub struct Mapping {
176    pub mapping_type: MappingType,
177    pub extra: BTreeMap<String, common::GenericValue>,
178    /// For a `map` field (a dynamic-key object), the mapping type of every
179    /// value; `None` for every other field. Internal metadata only — it is
180    /// **not** serialized into the index body (a map carries just
181    /// `{"type":"object","dynamic":true}`, the latter via `extra`). It exists so
182    /// a consumer turning the mapping into typed bindings can tell a `map` from a
183    /// plain `object` and offer a value-kind-typed handle.
184    pub map_values: Option<MappingType>,
185    /// Whether this numeric field came from a [`FlussoType::Decimal`] — a PG
186    /// `numeric`/`decimal`. It maps to OpenSearch `double` like a true `double`,
187    /// so [`mapping_type`](Self::mapping_type) alone can't tell them apart.
188    /// Internal metadata only — **not** serialized into the index body. It lets a
189    /// consumer turning the mapping into typed bindings offer a `Decimal`-kind
190    /// handle (exact) instead of an `f64`-kind one.
191    ///
192    /// [`FlussoType::Decimal`]: super::FlussoType::Decimal
193    pub decimal: bool,
194}
195
196/// Serializes the way OpenSearch expects a field mapping — `{ "type": …, …extra }`
197/// — rather than the struct's two named fields. The `extra` settings sit beside
198/// `type`, exactly as they would in the index body.
199impl Serialize for Mapping {
200    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
201        use serde::ser::SerializeMap;
202        let mut map = serializer.serialize_map(Some(1 + self.extra.len()))?;
203        map.serialize_entry("type", &self.mapping_type)?;
204        for (key, value) in &self.extra {
205            map.serialize_entry(key, value)?;
206        }
207        map.end()
208    }
209}
210
211#[derive(Debug, Clone, Hash, PartialEq, Eq)]
212pub enum MappingType {
213    Text,
214    Keyword,
215    Boolean,
216    Byte,
217    Short,
218    Integer,
219    Long,
220    Float,
221    Double,
222    HalfFloat,
223    ScaledFloat,
224    Date,
225    Object,
226    Nested,
227    /// Any mapping type not covered above.
228    Other(String),
229}
230
231impl MappingType {
232    /// The OpenSearch type name (`keyword`, `half_float`, …). An [`Other`] type
233    /// is its own verbatim name.
234    ///
235    /// [`Other`]: MappingType::Other
236    pub fn name(&self) -> &str {
237        match self {
238            MappingType::Text => "text",
239            MappingType::Keyword => "keyword",
240            MappingType::Boolean => "boolean",
241            MappingType::Byte => "byte",
242            MappingType::Short => "short",
243            MappingType::Integer => "integer",
244            MappingType::Long => "long",
245            MappingType::Float => "float",
246            MappingType::Double => "double",
247            MappingType::HalfFloat => "half_float",
248            MappingType::ScaledFloat => "scaled_float",
249            MappingType::Date => "date",
250            MappingType::Object => "object",
251            MappingType::Nested => "nested",
252            MappingType::Other(name) => name,
253        }
254    }
255
256    /// The mapping type for an OpenSearch type name — the inverse of
257    /// [`name`](Self::name). An unrecognized name becomes [`Other`].
258    ///
259    /// [`Other`]: MappingType::Other
260    pub fn from_name(name: &str) -> MappingType {
261        match name {
262            "text" => MappingType::Text,
263            "keyword" => MappingType::Keyword,
264            "boolean" => MappingType::Boolean,
265            "byte" => MappingType::Byte,
266            "short" => MappingType::Short,
267            "integer" => MappingType::Integer,
268            "long" => MappingType::Long,
269            "float" => MappingType::Float,
270            "double" => MappingType::Double,
271            "half_float" => MappingType::HalfFloat,
272            "scaled_float" => MappingType::ScaledFloat,
273            "date" => MappingType::Date,
274            "object" => MappingType::Object,
275            "nested" => MappingType::Nested,
276            other => MappingType::Other(other.to_owned()),
277        }
278    }
279}
280
281/// Serializes as the bare type name (`"keyword"`).
282/// Used instead of serde with inner at other because this code will not fail
283/// So having the name function will keep a single point of failure.
284impl Serialize for MappingType {
285    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
286        serializer.serialize_str(self.name())
287    }
288}