1mod conversion;
23mod entities;
24mod parser;
25
26pub use entities::*;
27pub use parser::ParseError;
28
29use serde::Deserialize;
30
31pub const SUPPORTED_VERSIONS: &[u8] = &[1];
32
33pub const INDEX_SCHEMA: &str = include_str!("../schemas/index.schema.yml");
38
39#[derive(thiserror::Error, Debug)]
40pub enum ConversionError {
41 #[error("invalid table name: {0}")]
42 TableName(#[from] schema_core::TableNameError),
43 #[error("invalid column name: {0}")]
44 ColumnName(#[from] schema_core::ColumnNameError),
45 #[error("invalid database schema name: {0}")]
46 DatabaseSchema(#[from] schema_core::DatabaseSchemaError),
47 #[error("`{verb}` join is missing its key: it takes {expected}")]
48 MissingJoinKey {
49 verb: &'static str,
50 expected: &'static str,
51 },
52 #[error("`{verb}` join does not take `{sibling}`; it takes {expected}")]
53 UnexpectedJoinKey {
54 verb: &'static str,
55 sibling: &'static str,
56 expected: &'static str,
57 },
58 #[error("`{verb}` join does not take `{sibling}` (a to-one join picks a single row)")]
59 UnexpectedJoinSibling {
60 verb: &'static str,
61 sibling: &'static str,
62 },
63 #[error("aggregate must specify either `foreign_key` or `through`, not both or neither")]
64 InvalidAggregateKey,
65 #[error("aggregate op '{op}' requires a `column`")]
66 MissingAggregateColumn { op: &'static str },
67 #[error("filter op '{op}' requires a value")]
68 MissingFilterValue { op: &'static str },
69 #[error("filter op 'between' requires exactly 2 values, got {got}")]
70 InvalidBetweenArity { got: usize },
71 #[error("filter op '{op}' requires a sequence value")]
72 ExpectedListValue { op: &'static str },
73 #[error("aggregate op '{op}' requires a `value_type` (its result mirrors the column)")]
74 MissingAggregateType { op: &'static str },
75 #[error(
76 "aggregate op '{op}' `value_type` must be a scalar type — `geo_point` and `custom` \
77 are not valid aggregate result types"
78 )]
79 InvalidAggregateType { op: &'static str },
80 #[error(
81 "aggregate op 'ids' requires an `element_type` (`long` or `keyword`) — it states the \
82 element type of the collected primary keys"
83 )]
84 MissingElementType,
85 #[error(
86 "aggregate op 'ids' `element_type` must be a scalar type — `geo_point` and `custom` \
87 are not valid element types"
88 )]
89 InvalidElementType,
90 #[error(
91 "aggregate op 'ids' does not take `{sibling}` (it always collects the related table's primary key)"
92 )]
93 UnexpectedIdsSibling { sibling: &'static str },
94 #[error("aggregate does not take `{sibling}` (only `ids` does)")]
95 UnexpectedAggregateSibling { sibling: &'static str },
96 #[error(
97 "a `geo` field needs either both `lat` and `lon` (two columns) or a single `column` \
98 holding a combined value — not a mix"
99 )]
100 InvalidGeoSource,
101}
102
103#[derive(Debug, Clone, Deserialize)]
104#[serde(deny_unknown_fields)]
105pub struct SchemaYaml {
106 pub version: u8,
107 pub table: String,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub schema: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub primary_key: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub doc_id: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub soft_delete: Option<SoftDelete>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub filters: Option<Vec<Filter>>,
119 pub fields: Vec<Field>,
120}
121
122impl TryFrom<SchemaYaml> for schema_core::IndexSchema {
123 type Error = ConversionError;
124
125 fn try_from(yaml: SchemaYaml) -> Result<Self, Self::Error> {
126 use schema_core::common::{ColumnName, TableName};
127
128 let table = TableName::try_new(yaml.table)?;
129 let db_schema = match yaml.schema {
130 Some(s) => schema_core::DatabaseSchema::try_new(s)?,
131 None => schema_core::DatabaseSchema::default(),
132 };
133 let primary_key = yaml.primary_key.map(ColumnName::try_new).transpose()?;
134 let doc_id = yaml.doc_id.map(ColumnName::try_new).transpose()?;
135 let soft_delete = yaml
136 .soft_delete
137 .map(conversion::convert_soft_delete)
138 .transpose()?;
139 let filters = conversion::convert_filters_opt(yaml.filters)?;
140 let fields = yaml
141 .fields
142 .into_iter()
143 .map(conversion::convert_field)
144 .collect::<Result<_, _>>()?;
145
146 Ok(schema_core::IndexSchema {
147 version: yaml.version,
148 table,
149 db_schema,
150 primary_key,
151 doc_id,
152 soft_delete,
153 filters,
154 fields,
155 })
156 }
157}