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