1use super::ir::AuthoringIR;
13use crate::{
14 error::{FraiseQLError, Result},
15 schema::is_known_scalar,
16};
17
18pub(crate) fn extract_base_type(type_str: &str) -> &str {
27 let s = type_str.trim();
28
29 let s = s.trim_start_matches('[').trim_end_matches(']');
31 let s = s.trim_end_matches('!').trim_start_matches('!');
32
33 let s = s.trim_start_matches('[').trim_end_matches(']');
35 let s = s.trim_end_matches('!');
36
37 s.trim()
38}
39
40fn is_valid_type(base_type: &str, defined_types: &std::collections::HashSet<&str>) -> bool {
42 is_known_scalar(base_type) || defined_types.contains(base_type)
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct SchemaValidationError {
48 pub message: String,
50 pub location: String,
52}
53
54pub struct SchemaValidator {
56 }
58
59impl SchemaValidator {
60 #[must_use]
62 pub const fn new() -> Self {
63 Self {}
64 }
65
66 pub fn validate(&self, ir: AuthoringIR) -> Result<AuthoringIR> {
80 self.validate_types(&ir)?;
84 self.validate_queries(&ir)?;
85
86 if !ir.fact_tables.is_empty() {
88 self.validate_fact_tables(&ir)?;
89 }
90
91 self.validate_aggregate_types(&ir)?;
94
95 Ok(ir)
96 }
97
98 fn validate_types(&self, ir: &AuthoringIR) -> Result<()> {
100 let defined_types: std::collections::HashSet<&str> =
102 ir.types.iter().map(|t| t.name.as_str()).collect();
103
104 for ir_type in &ir.types {
106 if ir_type.name.is_empty() {
108 return Err(FraiseQLError::Validation {
109 message: "Type name cannot be empty".to_string(),
110 path: Some("types".to_string()),
111 });
112 }
113
114 for field in &ir_type.fields {
116 let base_type = extract_base_type(&field.field_type);
117
118 if !base_type.is_empty() && !is_valid_type(base_type, &defined_types) {
120 return Err(FraiseQLError::Validation {
121 message: format!(
122 "Type '{}' field '{}' references unknown type '{}'",
123 ir_type.name, field.name, base_type
124 ),
125 path: Some(format!("types.{}.fields.{}", ir_type.name, field.name)),
126 });
127 }
128 }
129 }
130
131 Ok(())
132 }
133
134 fn validate_queries(&self, ir: &AuthoringIR) -> Result<()> {
136 let defined_types: std::collections::HashSet<&str> =
138 ir.types.iter().map(|t| t.name.as_str()).collect();
139
140 for query in &ir.queries {
142 if query.name.is_empty() {
144 return Err(FraiseQLError::Validation {
145 message: "Query name cannot be empty".to_string(),
146 path: Some("queries".to_string()),
147 });
148 }
149
150 let base_type = extract_base_type(&query.return_type);
152 if !is_valid_type(base_type, &defined_types) {
153 return Err(FraiseQLError::Validation {
154 message: format!(
155 "Query '{}' returns unknown type '{}'",
156 query.name, query.return_type
157 ),
158 path: Some(format!("queries.{}.return_type", query.name)),
159 });
160 }
161
162 for arg in &query.arguments {
164 let base_type = extract_base_type(&arg.arg_type);
165 if !is_valid_type(base_type, &defined_types) {
166 return Err(FraiseQLError::Validation {
167 message: format!(
168 "Query '{}' argument '{}' has unknown type '{}'",
169 query.name, arg.name, arg.arg_type
170 ),
171 path: Some(format!("queries.{}.arguments.{}", query.name, arg.name)),
172 });
173 }
174 }
175 }
176
177 for mutation in &ir.mutations {
179 if mutation.name.is_empty() {
180 return Err(FraiseQLError::Validation {
181 message: "Mutation name cannot be empty".to_string(),
182 path: Some("mutations".to_string()),
183 });
184 }
185
186 let base_type = extract_base_type(&mutation.return_type);
187 if !is_valid_type(base_type, &defined_types) {
188 return Err(FraiseQLError::Validation {
189 message: format!(
190 "Mutation '{}' returns unknown type '{}'",
191 mutation.name, mutation.return_type
192 ),
193 path: Some(format!("mutations.{}.return_type", mutation.name)),
194 });
195 }
196 }
197
198 for subscription in &ir.subscriptions {
200 if subscription.name.is_empty() {
201 return Err(FraiseQLError::Validation {
202 message: "Subscription name cannot be empty".to_string(),
203 path: Some("subscriptions".to_string()),
204 });
205 }
206
207 let base_type = extract_base_type(&subscription.return_type);
208 if !is_valid_type(base_type, &defined_types) {
209 return Err(FraiseQLError::Validation {
210 message: format!(
211 "Subscription '{}' returns unknown type '{}'",
212 subscription.name, subscription.return_type
213 ),
214 path: Some(format!("subscriptions.{}.return_type", subscription.name)),
215 });
216 }
217 }
218
219 Ok(())
220 }
221
222 fn validate_fact_tables(&self, ir: &AuthoringIR) -> Result<()> {
228 for (table_name, metadata) in &ir.fact_tables {
229 if !table_name.starts_with("tf_") {
231 return Err(FraiseQLError::Validation {
232 message: format!("Fact table '{}' must start with 'tf_' prefix", table_name),
233 path: Some(format!("fact_tables.{}", table_name)),
234 });
235 }
236
237 if metadata.measures.is_empty() {
238 return Err(FraiseQLError::Validation {
239 message: format!("Fact table '{}' must have at least one measure", table_name),
240 path: Some(format!("fact_tables.{}.measures", table_name)),
241 });
242 }
243
244 if metadata.dimensions.name.is_empty() {
246 return Err(FraiseQLError::Validation {
247 message: format!("Fact table '{}' dimensions missing 'name' field", table_name),
248 path: Some(format!("fact_tables.{}.dimensions", table_name)),
249 });
250 }
251 }
252
253 Ok(())
254 }
255
256 fn validate_aggregate_types(&self, ir: &AuthoringIR) -> Result<()> {
264 for ir_type in &ir.types {
266 if ir_type.name.ends_with("Aggregate") {
267 let has_count = ir_type.fields.iter().any(|f| f.name == "count");
269 if !has_count {
270 return Err(FraiseQLError::Validation {
271 message: format!(
272 "Aggregate type '{}' must have a 'count' field",
273 ir_type.name
274 ),
275 path: Some(format!("types.{}.fields", ir_type.name)),
276 });
277 }
278 }
279
280 if ir_type.name.ends_with("GroupByInput") {
282 for field in &ir_type.fields {
283 if field.field_type != "Boolean" && field.field_type != "Boolean!" {
285 return Err(FraiseQLError::Validation {
286 message: format!(
287 "GroupByInput type '{}' field '{}' must be Boolean, got '{}'",
288 ir_type.name, field.name, field.field_type
289 ),
290 path: Some(format!("types.{}.fields.{}", ir_type.name, field.name)),
291 });
292 }
293 }
294 }
295
296 if ir_type.name.ends_with("HavingInput") {
298 for field in &ir_type.fields {
299 let valid_suffixes = ["_eq", "_neq", "_gt", "_gte", "_lt", "_lte"];
301 let has_valid_suffix = valid_suffixes.iter().any(|s| field.name.ends_with(s));
302
303 if !has_valid_suffix {
304 return Err(FraiseQLError::Validation {
305 message: format!(
306 "HavingInput type '{}' field '{}' must have operator suffix (_eq, _neq, _gt, _gte, _lt, _lte)",
307 ir_type.name, field.name
308 ),
309 path: Some(format!("types.{}.fields.{}", ir_type.name, field.name)),
310 });
311 }
312 }
313 }
314 }
315
316 Ok(())
317 }
318}
319
320impl Default for SchemaValidator {
321 fn default() -> Self {
322 Self::new()
323 }
324}