1use std::collections::HashMap;
26
27use crate::{crs::Crs, geometry::GeometryType, value::ValueType, Error, Result};
28
29#[derive(Debug, Clone, PartialEq)]
30pub struct FieldDef {
31 pub name: String,
32 pub alias: Option<String>,
33 pub ty: ValueType,
34 pub nullable: bool,
35 pub width: Option<u32>,
37}
38
39impl FieldDef {
40 pub fn new(name: impl Into<String>, ty: ValueType, nullable: bool) -> Self {
41 Self {
42 name: name.into(),
43 alias: None,
44 ty,
45 nullable,
46 width: None,
47 }
48 }
49
50 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
51 self.alias = Some(alias.into());
52 self
53 }
54
55 pub fn with_width(mut self, width: u32) -> Self {
56 self.width = Some(width);
57 self
58 }
59}
60
61#[derive(Debug, Clone, PartialEq)]
63pub struct GeomField {
64 pub name: String,
65 pub kind: GeometryType,
66 pub has_z: bool,
67 pub has_m: bool,
68 pub extent: Option<[f64; 6]>,
70}
71
72impl GeomField {
73 pub fn new(name: impl Into<String>, kind: GeometryType) -> Self {
74 Self {
75 name: name.into(),
76 kind,
77 has_z: false,
78 has_m: false,
79 extent: None,
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
85pub struct Schema {
86 pub fields: Vec<FieldDef>,
87 pub geometry: Option<GeomField>,
88 pub crs: Crs,
89 name_index: HashMap<String, usize>,
90}
91
92impl Schema {
93 pub fn new(fields: Vec<FieldDef>, geometry: Option<GeomField>, crs: Crs) -> Self {
94 let name_index = fields
95 .iter()
96 .enumerate()
97 .map(|(i, f)| (f.name.clone(), i))
98 .collect();
99 Self {
100 fields,
101 geometry,
102 crs,
103 name_index,
104 }
105 }
106
107 pub fn field(&self, idx: usize) -> Option<&FieldDef> {
108 self.fields.get(idx)
109 }
110
111 pub fn field_index(&self, name: &str) -> Option<usize> {
112 self.name_index.get(name).copied()
113 }
114
115 pub fn field_by_name(&self, name: &str) -> Option<&FieldDef> {
116 self.field_index(name).and_then(|i| self.fields.get(i))
117 }
118
119 pub fn len(&self) -> usize {
120 self.fields.len()
121 }
122
123 pub fn is_empty(&self) -> bool {
124 self.fields.is_empty()
125 }
126
127 pub fn validate_row(&self, values: &[crate::Value]) -> Result<()> {
130 if values.len() != self.fields.len() {
131 return Err(Error::schema(format!(
132 "expected {} values, got {}",
133 self.fields.len(),
134 values.len()
135 )));
136 }
137 for (i, (v, f)) in values.iter().zip(&self.fields).enumerate() {
138 if v.is_null() {
139 if !f.nullable {
140 return Err(Error::schema(format!(
141 "field {} ({}) is not nullable but value is null",
142 i, f.name
143 )));
144 }
145 continue;
146 }
147 match v.ty() {
148 Some(t) if t == f.ty => {}
149 Some(t) => {
150 return Err(Error::schema(format!(
151 "field {} ({}) expected {:?}, got {:?}",
152 i, f.name, f.ty, t
153 )))
154 }
155 None => unreachable!("non-null value must have a type"),
156 }
157 }
158 Ok(())
159 }
160}
161
162impl PartialEq for Schema {
163 fn eq(&self, other: &Self) -> bool {
164 self.fields == other.fields && self.geometry == other.geometry && self.crs == other.crs
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::Value;
172
173 fn sample() -> Schema {
174 Schema::new(
175 vec![
176 FieldDef::new("id", ValueType::Int64, false),
177 FieldDef::new("name", ValueType::String, true),
178 ],
179 Some(GeomField::new("geom", GeometryType::Point)),
180 Crs::Epsg(4326),
181 )
182 }
183
184 #[test]
185 fn name_index_lookup() {
186 let s = sample();
187 assert_eq!(s.field_index("id"), Some(0));
188 assert_eq!(s.field_index("name"), Some(1));
189 assert_eq!(s.field_index("missing"), None);
190 assert_eq!(s.field_by_name("id").unwrap().ty, ValueType::Int64);
191 }
192
193 #[test]
194 fn validate_row_happy_path() {
195 let s = sample();
196 s.validate_row(&[Value::Int64(1), Value::String("a".into())])
197 .unwrap();
198 s.validate_row(&[Value::Int64(1), Value::Null]).unwrap();
199 }
200
201 #[test]
202 fn validate_row_rejects_null_in_non_nullable() {
203 let s = sample();
204 assert!(s
205 .validate_row(&[Value::Null, Value::String("a".into())])
206 .is_err());
207 }
208
209 #[test]
210 fn validate_row_rejects_arity_mismatch() {
211 let s = sample();
212 assert!(s.validate_row(&[Value::Int64(1)]).is_err());
213 }
214
215 #[test]
216 fn validate_row_rejects_type_mismatch() {
217 let s = sample();
218 assert!(s
219 .validate_row(&[Value::String("nope".into()), Value::Null])
220 .is_err());
221 }
222}