1use std::collections::HashMap;
2
3use pest::Parser;
4use pest::error::InputLocation;
5use pest_derive::Parser;
6
7use crate::error::{
8 NanoError, ParseDiagnostic, Result, SourceSpan, decode_string_literal, render_span,
9};
10use crate::types::{PropType, ScalarType};
11
12use super::ast::*;
13
14#[derive(Parser)]
15#[grammar = "schema/schema.pest"]
16struct SchemaParser;
17
18pub fn parse_schema(input: &str) -> Result<SchemaFile> {
19 parse_schema_diagnostic(input).map_err(|e| NanoError::Parse(e.to_string()))
20}
21
22pub fn parse_schema_diagnostic(input: &str) -> std::result::Result<SchemaFile, ParseDiagnostic> {
23 let pairs = SchemaParser::parse(Rule::schema_file, input).map_err(pest_error_to_diagnostic)?;
24
25 let mut declarations = Vec::new();
26 for pair in pairs {
27 if pair.as_rule() == Rule::schema_file {
28 for inner in pair.into_inner() {
29 if let Rule::schema_decl = inner.as_rule() {
30 declarations.push(parse_schema_decl(inner).map_err(nano_error_to_diagnostic)?);
31 }
32 }
33 }
34 }
35
36 let interfaces: Vec<InterfaceDecl> = declarations
38 .iter()
39 .filter_map(|d| match d {
40 SchemaDecl::Interface(i) => Some(i.clone()),
41 _ => None,
42 })
43 .collect();
44
45 let iface_refs: Vec<&InterfaceDecl> = interfaces.iter().collect();
47 for decl in &mut declarations {
48 if let SchemaDecl::Node(node) = decl {
49 resolve_interfaces(node, &iface_refs).map_err(nano_error_to_diagnostic)?;
50 }
51 }
52
53 let schema = SchemaFile { declarations };
54 validate_schema_annotations(&schema).map_err(nano_error_to_diagnostic)?;
55 validate_constraints(&schema).map_err(nano_error_to_diagnostic)?;
56 Ok(schema)
57}
58
59fn pest_error_to_diagnostic(err: pest::error::Error<Rule>) -> ParseDiagnostic {
60 let span = match err.location {
61 InputLocation::Pos(pos) => Some(render_span(SourceSpan::new(pos, pos))),
62 InputLocation::Span((start, end)) => Some(render_span(SourceSpan::new(start, end))),
63 };
64 ParseDiagnostic::new(err.to_string(), span)
65}
66
67fn nano_error_to_diagnostic(err: NanoError) -> ParseDiagnostic {
68 ParseDiagnostic::new(err.to_string(), None)
69}
70
71fn parse_schema_decl(pair: pest::iterators::Pair<Rule>) -> Result<SchemaDecl> {
72 let inner = pair.into_inner().next().unwrap();
73 match inner.as_rule() {
74 Rule::interface_decl => Ok(SchemaDecl::Interface(parse_interface_decl(inner)?)),
75 Rule::node_decl => Ok(SchemaDecl::Node(parse_node_decl(inner)?)),
76 Rule::edge_decl => Ok(SchemaDecl::Edge(parse_edge_decl(inner)?)),
77 _ => Err(NanoError::Parse(format!(
78 "unexpected rule: {:?}",
79 inner.as_rule()
80 ))),
81 }
82}
83
84fn parse_interface_decl(pair: pest::iterators::Pair<Rule>) -> Result<InterfaceDecl> {
85 let mut inner = pair.into_inner();
86 let name = inner.next().unwrap().as_str().to_string();
87
88 let mut properties = Vec::new();
89 for item in inner {
90 if let Rule::prop_decl = item.as_rule() {
91 properties.push(parse_prop_decl(item)?);
92 }
93 }
94
95 Ok(InterfaceDecl { name, properties })
96}
97
98fn parse_node_decl(pair: pest::iterators::Pair<Rule>) -> Result<NodeDecl> {
99 let mut inner = pair.into_inner();
100 let name = inner.next().unwrap().as_str().to_string();
101
102 let mut annotations = Vec::new();
103 let mut implements = Vec::new();
104 let mut properties = Vec::new();
105 let mut constraints = Vec::new();
106
107 for item in inner {
108 match item.as_rule() {
109 Rule::annotation => {
110 annotations.push(parse_annotation(item)?);
111 }
112 Rule::implements_clause => {
113 for iface in item.into_inner() {
114 if iface.as_rule() == Rule::type_name {
115 implements.push(iface.as_str().to_string());
116 }
117 }
118 }
119 Rule::prop_decl => {
120 properties.push(parse_prop_decl(item)?);
121 }
122 Rule::body_constraint => {
123 constraints.push(parse_body_constraint(item)?);
124 }
125 _ => {}
126 }
127 }
128
129 desugar_property_constraints(&properties, &mut constraints);
131
132 Ok(NodeDecl {
133 name,
134 annotations,
135 implements,
136 properties,
137 constraints,
138 })
139}
140
141fn parse_edge_decl(pair: pest::iterators::Pair<Rule>) -> Result<EdgeDecl> {
142 let mut inner = pair.into_inner();
143 let name = inner.next().unwrap().as_str().to_string();
144 let from_type = inner.next().unwrap().as_str().to_string();
145 let to_type = inner.next().unwrap().as_str().to_string();
146
147 let mut cardinality = Cardinality::default();
148 let mut annotations = Vec::new();
149 let mut properties = Vec::new();
150 let mut constraints = Vec::new();
151
152 for item in inner {
153 match item.as_rule() {
154 Rule::cardinality => {
155 cardinality = parse_cardinality(item)?;
156 }
157 Rule::annotation => annotations.push(parse_annotation(item)?),
158 Rule::prop_decl => properties.push(parse_prop_decl(item)?),
159 Rule::body_constraint => constraints.push(parse_body_constraint(item)?),
160 _ => {}
161 }
162 }
163
164 desugar_property_constraints(&properties, &mut constraints);
166
167 Ok(EdgeDecl {
168 name,
169 from_type,
170 to_type,
171 cardinality,
172 annotations,
173 properties,
174 constraints,
175 })
176}
177
178fn parse_cardinality(pair: pest::iterators::Pair<Rule>) -> Result<Cardinality> {
179 let mut inner = pair.into_inner();
180 let min_str = inner.next().unwrap().as_str();
181 let min = min_str
182 .parse::<u32>()
183 .map_err(|_| NanoError::Parse(format!("invalid cardinality min: {}", min_str)))?;
184 let max = if let Some(max_pair) = inner.next() {
185 let max_str = max_pair.as_str();
186 Some(
187 max_str
188 .parse::<u32>()
189 .map_err(|_| NanoError::Parse(format!("invalid cardinality max: {}", max_str)))?,
190 )
191 } else {
192 None
193 };
194
195 if let Some(max_val) = max {
196 if min > max_val {
197 return Err(NanoError::Parse(format!(
198 "cardinality min ({}) exceeds max ({})",
199 min, max_val
200 )));
201 }
202 }
203
204 Ok(Cardinality { min, max })
205}
206
207fn parse_body_constraint(pair: pest::iterators::Pair<Rule>) -> Result<Constraint> {
208 let mut inner = pair.into_inner();
209 let name_pair = inner.next().unwrap();
210 let constraint_name = name_pair.as_str();
211 let args_pair = inner.next().unwrap();
212 let args: Vec<pest::iterators::Pair<Rule>> = args_pair.into_inner().collect();
213
214 match constraint_name {
215 "key" => {
216 let names: Vec<String> = args
217 .into_iter()
218 .filter(|a| a.as_rule() == Rule::ident || a.as_rule() == Rule::constraint_arg)
219 .map(|a| extract_ident_from_constraint_arg(a))
220 .collect::<Result<Vec<_>>>()?;
221 if names.is_empty() {
222 return Err(NanoError::Parse(
223 "@key constraint requires at least one property name".to_string(),
224 ));
225 }
226 Ok(Constraint::Key(names))
227 }
228 "unique" => {
229 let names = extract_ident_list_from_args(args)?;
230 if names.is_empty() {
231 return Err(NanoError::Parse(
232 "@unique constraint requires at least one property name".to_string(),
233 ));
234 }
235 Ok(Constraint::Unique(names))
236 }
237 "index" => {
238 let names = extract_ident_list_from_args(args)?;
239 if names.is_empty() {
240 return Err(NanoError::Parse(
241 "@index constraint requires at least one property name".to_string(),
242 ));
243 }
244 Ok(Constraint::Index(names))
245 }
246 "range" => {
247 if args.len() < 2 {
249 return Err(NanoError::Parse(
250 "@range requires property name and bounds: @range(prop, min..max)".to_string(),
251 ));
252 }
253 let property = extract_ident_from_constraint_arg(args[0].clone())?;
254 let (min, max) = extract_range_bounds(&args[1])?;
256 Ok(Constraint::Range { property, min, max })
257 }
258 "check" => {
259 if args.len() < 2 {
261 return Err(NanoError::Parse(
262 "@check requires property name and pattern: @check(prop, \"regex\")"
263 .to_string(),
264 ));
265 }
266 let property = extract_ident_from_constraint_arg(args[0].clone())?;
267 let pattern = extract_string_from_constraint_arg(&args[1])?;
268 Ok(Constraint::Check { property, pattern })
269 }
270 other => Err(NanoError::Parse(format!("unknown constraint: @{}", other))),
271 }
272}
273
274fn extract_ident_from_constraint_arg(pair: pest::iterators::Pair<Rule>) -> Result<String> {
275 if pair.as_rule() == Rule::ident {
276 return Ok(pair.as_str().to_string());
277 }
278 if let Some(inner) = pair.into_inner().next() {
280 if inner.as_rule() == Rule::ident {
281 return Ok(inner.as_str().to_string());
282 }
283 }
284 Err(NanoError::Parse(
285 "expected property name in constraint".to_string(),
286 ))
287}
288
289fn extract_ident_list_from_args(args: Vec<pest::iterators::Pair<Rule>>) -> Result<Vec<String>> {
290 let mut names = Vec::new();
291 for arg in args {
292 names.push(extract_ident_from_constraint_arg(arg)?);
293 }
294 Ok(names)
295}
296
297fn extract_string_from_constraint_arg(pair: &pest::iterators::Pair<Rule>) -> Result<String> {
298 fn find_string(pair: &pest::iterators::Pair<Rule>) -> Result<Option<String>> {
300 if pair.as_rule() == Rule::string_lit {
301 return decode_string_literal(pair.as_str()).map(Some);
302 }
303 for inner in pair.clone().into_inner() {
304 if let Some(s) = find_string(&inner)? {
305 return Ok(Some(s));
306 }
307 }
308 Ok(None)
309 }
310
311 find_string(pair)?
312 .ok_or_else(|| NanoError::Parse("expected string argument in constraint".to_string()))
313}
314
315fn extract_range_bounds(
316 pair: &pest::iterators::Pair<Rule>,
317) -> Result<(Option<ConstraintBound>, Option<ConstraintBound>)> {
318 let range_pair = if pair.as_rule() == Rule::range_bound {
320 pair.clone()
321 } else {
322 let mut found = None;
323 for inner in pair.clone().into_inner() {
324 if inner.as_rule() == Rule::range_bound {
325 found = Some(inner);
326 break;
327 }
328 }
329 found.ok_or_else(|| {
330 NanoError::Parse("expected range bounds (min..max) in @range constraint".to_string())
331 })?
332 };
333
334 let mut min = None;
335 let mut max = None;
336 let mut seen_bound = false;
337
338 for child in range_pair.into_inner() {
339 if child.as_rule() == Rule::literal
340 || child.as_rule() == Rule::integer
341 || child.as_rule() == Rule::float_lit
342 || child.as_rule() == Rule::signed_integer
343 || child.as_rule() == Rule::signed_float
344 {
345 let bound = parse_constraint_bound(&child)?;
346 if !seen_bound {
347 min = Some(bound);
348 seen_bound = true;
349 } else {
350 max = Some(bound);
351 }
352 }
353 }
354
355 Ok((min, max))
356}
357
358fn parse_constraint_bound(pair: &pest::iterators::Pair<Rule>) -> Result<ConstraintBound> {
359 let text = pair.as_str();
360
361 if let Ok(n) = text.parse::<i64>() {
363 return Ok(ConstraintBound::Integer(n));
364 }
365 if let Ok(f) = text.parse::<f64>() {
367 return Ok(ConstraintBound::Float(f));
368 }
369
370 for inner in pair.clone().into_inner() {
372 let s = inner.as_str();
373 if let Ok(n) = s.parse::<i64>() {
374 return Ok(ConstraintBound::Integer(n));
375 }
376 if let Ok(f) = s.parse::<f64>() {
377 return Ok(ConstraintBound::Float(f));
378 }
379 }
380
381 Err(NanoError::Parse(format!(
382 "invalid constraint bound: {}",
383 text
384 )))
385}
386
387fn desugar_property_constraints(properties: &[PropDecl], constraints: &mut Vec<Constraint>) {
389 for prop in properties {
390 for ann in &prop.annotations {
391 match ann.name.as_str() {
392 "key" if ann.value.is_none() => {
393 constraints.push(Constraint::Key(vec![prop.name.clone()]));
394 }
395 "unique" if ann.value.is_none() => {
396 constraints.push(Constraint::Unique(vec![prop.name.clone()]));
397 }
398 "index" if ann.value.is_none() => {
399 constraints.push(Constraint::Index(vec![prop.name.clone()]));
400 }
401 _ => {}
402 }
403 }
404 }
405}
406
407fn resolve_interfaces(node: &mut NodeDecl, interfaces: &[&InterfaceDecl]) -> Result<()> {
409 let interface_map: HashMap<&str, &InterfaceDecl> =
410 interfaces.iter().map(|i| (i.name.as_str(), *i)).collect();
411
412 for iface_name in &node.implements {
413 let iface = interface_map.get(iface_name.as_str()).ok_or_else(|| {
414 NanoError::Parse(format!(
415 "node {} implements unknown interface '{}'",
416 node.name, iface_name
417 ))
418 })?;
419
420 for iface_prop in &iface.properties {
421 if let Some(existing) = node.properties.iter().find(|p| p.name == iface_prop.name) {
422 if existing.prop_type != iface_prop.prop_type {
424 return Err(NanoError::Parse(format!(
425 "node {} property '{}' has type {} but interface {} declares it as {}",
426 node.name,
427 iface_prop.name,
428 existing.prop_type.display_name(),
429 iface_name,
430 iface_prop.prop_type.display_name()
431 )));
432 }
433 } else {
434 node.properties.push(iface_prop.clone());
436 desugar_property_constraints(
438 std::slice::from_ref(iface_prop),
439 &mut node.constraints,
440 );
441 }
442 }
443 }
444
445 Ok(())
446}
447
448fn parse_prop_decl(pair: pest::iterators::Pair<Rule>) -> Result<PropDecl> {
449 let mut inner = pair.into_inner();
450 let name = inner.next().unwrap().as_str().to_string();
451 let type_ref = inner.next().unwrap();
452 let prop_type = parse_type_ref(type_ref)?;
453
454 let mut annotations = Vec::new();
455 for item in inner {
456 if let Rule::annotation = item.as_rule() {
457 annotations.push(parse_annotation(item)?);
458 }
459 }
460
461 Ok(PropDecl {
462 name,
463 prop_type,
464 annotations,
465 })
466}
467
468fn parse_type_ref(pair: pest::iterators::Pair<Rule>) -> Result<PropType> {
469 let text = pair.as_str();
470 let nullable = text.ends_with('?');
471
472 let mut inner = pair
473 .into_inner()
474 .next()
475 .ok_or_else(|| NanoError::Parse("type reference is missing core type".to_string()))?;
476 if inner.as_rule() == Rule::core_type {
477 inner = inner
478 .into_inner()
479 .next()
480 .ok_or_else(|| NanoError::Parse("type reference is missing core type".to_string()))?;
481 }
482
483 match inner.as_rule() {
484 Rule::base_type => {
485 let scalar = ScalarType::from_str_name(inner.as_str())
486 .ok_or_else(|| NanoError::Parse(format!("unknown type: {}", inner.as_str())))?;
487 Ok(PropType::scalar(scalar, nullable))
488 }
489 Rule::vector_type => {
490 let dim_text = inner
491 .into_inner()
492 .next()
493 .ok_or_else(|| NanoError::Parse("Vector type missing dimension".to_string()))?
494 .as_str();
495 let dim = dim_text
496 .parse::<u32>()
497 .map_err(|e| NanoError::Parse(format!("invalid Vector dimension: {}", e)))?;
498 if dim == 0 {
499 return Err(NanoError::Parse(
500 "Vector dimension must be greater than zero".to_string(),
501 ));
502 }
503 if dim > i32::MAX as u32 {
504 return Err(NanoError::Parse(format!(
505 "Vector dimension {} exceeds maximum supported {}",
506 dim,
507 i32::MAX
508 )));
509 }
510 Ok(PropType::scalar(ScalarType::Vector(dim), nullable))
511 }
512 Rule::list_type => {
513 let element = inner
514 .into_inner()
515 .next()
516 .ok_or_else(|| NanoError::Parse("list type missing element type".to_string()))?;
517 let scalar = ScalarType::from_str_name(element.as_str()).ok_or_else(|| {
518 NanoError::Parse(format!("unknown list element type: {}", element.as_str()))
519 })?;
520 if matches!(scalar, ScalarType::Blob) {
521 return Err(NanoError::Parse(
522 "list of Blob is not supported".to_string(),
523 ));
524 }
525 Ok(PropType::list_of(scalar, nullable))
526 }
527 Rule::enum_type => {
528 let mut values = Vec::new();
529 for value in inner.into_inner() {
530 if value.as_rule() == Rule::enum_value {
531 values.push(value.as_str().to_string());
532 }
533 }
534 if values.is_empty() {
535 return Err(NanoError::Parse(
536 "enum type must include at least one value".to_string(),
537 ));
538 }
539 let mut dedup = values.clone();
540 dedup.sort();
541 dedup.dedup();
542 if dedup.len() != values.len() {
543 return Err(NanoError::Parse(
544 "enum type cannot include duplicate values".to_string(),
545 ));
546 }
547 Ok(PropType::enum_type(values, nullable))
548 }
549 other => Err(NanoError::Parse(format!(
550 "unexpected type rule: {:?}",
551 other
552 ))),
553 }
554}
555
556fn parse_annotation(pair: pest::iterators::Pair<Rule>) -> Result<Annotation> {
557 let mut inner = pair.into_inner();
558 let name = inner.next().unwrap().as_str().to_string();
559 let value = inner
560 .next()
561 .map(|p| decode_string_literal(p.as_str()))
562 .transpose()?;
563
564 Ok(Annotation { name, value })
565}
566
567fn validate_string_annotation(
568 annotations: &[Annotation],
569 annotation: &str,
570 target: &str,
571) -> Result<()> {
572 let mut seen = false;
573 for ann in annotations {
574 if ann.name != annotation {
575 continue;
576 }
577 if seen {
578 return Err(NanoError::Parse(format!(
579 "{} declares @{} multiple times",
580 target, annotation
581 )));
582 }
583 let value = ann.value.as_deref().ok_or_else(|| {
584 NanoError::Parse(format!(
585 "@{} on {} requires a non-empty value",
586 annotation, target
587 ))
588 })?;
589 if value.trim().is_empty() {
590 return Err(NanoError::Parse(format!(
591 "@{} on {} requires a non-empty value",
592 annotation, target
593 )));
594 }
595 seen = true;
596 }
597 Ok(())
598}
599
600fn validate_schema_annotations(schema: &SchemaFile) -> Result<()> {
603 for decl in &schema.declarations {
604 match decl {
605 SchemaDecl::Interface(_) => {} SchemaDecl::Node(node) => {
607 for ann in &node.annotations {
609 if ann.name == "key"
610 || ann.name == "unique"
611 || ann.name == "index"
612 || ann.name == "embed"
613 {
614 return Err(NanoError::Parse(format!(
615 "@{} is only supported on node properties or as body constraint (node {})",
616 ann.name, node.name
617 )));
618 }
619 }
620 validate_string_annotation(
621 &node.annotations,
622 "description",
623 &format!("node {}", node.name),
624 )?;
625 validate_string_annotation(
626 &node.annotations,
627 "instruction",
628 &format!("node {}", node.name),
629 )?;
630
631 for prop in &node.properties {
633 validate_property_annotations(prop, &node.name, &node.properties, false)?;
634 }
635 }
636 SchemaDecl::Edge(edge) => {
637 for ann in &edge.annotations {
638 if ann.name == "key"
639 || ann.name == "unique"
640 || ann.name == "index"
641 || ann.name == "embed"
642 {
643 return Err(NanoError::Parse(format!(
644 "@{} is not supported on edges (edge {})",
645 ann.name, edge.name
646 )));
647 }
648 }
649 validate_string_annotation(
650 &edge.annotations,
651 "description",
652 &format!("edge {}", edge.name),
653 )?;
654 validate_string_annotation(
655 &edge.annotations,
656 "instruction",
657 &format!("edge {}", edge.name),
658 )?;
659
660 for prop in &edge.properties {
661 validate_property_annotations(prop, &edge.name, &edge.properties, true)?;
662 }
663 }
664 }
665 }
666 Ok(())
667}
668
669fn validate_property_annotations(
670 prop: &PropDecl,
671 type_name: &str,
672 all_properties: &[PropDecl],
673 is_edge: bool,
674) -> Result<()> {
675 let is_vector = matches!(prop.prop_type.scalar, ScalarType::Vector(_));
676 let is_blob = matches!(prop.prop_type.scalar, ScalarType::Blob);
677
678 validate_string_annotation(
679 &prop.annotations,
680 "description",
681 &format!("property {}.{}", type_name, prop.name),
682 )?;
683
684 let mut key_seen = false;
685 let mut unique_seen = false;
686 let mut index_seen = false;
687 let mut embed_seen = false;
688
689 for ann in &prop.annotations {
690 if prop.prop_type.list
692 && (ann.name == "key"
693 || ann.name == "unique"
694 || ann.name == "index"
695 || ann.name == "embed")
696 {
697 return Err(NanoError::Parse(format!(
698 "@{} is not supported on list property {}.{}",
699 ann.name, type_name, prop.name
700 )));
701 }
702 if is_vector && (ann.name == "key" || ann.name == "unique") {
703 return Err(NanoError::Parse(format!(
704 "@{} is not supported on vector property {}.{}",
705 ann.name, type_name, prop.name
706 )));
707 }
708 if is_blob
709 && (ann.name == "key"
710 || ann.name == "unique"
711 || ann.name == "index"
712 || ann.name == "embed")
713 {
714 return Err(NanoError::Parse(format!(
715 "@{} is not supported on blob property {}.{}",
716 ann.name, type_name, prop.name
717 )));
718 }
719 if ann.name == "instruction" {
720 return Err(NanoError::Parse(format!(
721 "@instruction is only supported on node and edge types (property {}.{})",
722 type_name, prop.name
723 )));
724 }
725
726 if is_edge && (ann.name == "key" || ann.name == "embed") {
728 return Err(NanoError::Parse(format!(
729 "@{} is not supported on edge properties (edge {}.{})",
730 ann.name, type_name, prop.name
731 )));
732 }
733
734 match ann.name.as_str() {
736 "key" => {
737 if ann.value.is_some() {
738 return Err(NanoError::Parse(format!(
739 "@key on {}.{} does not accept a value",
740 type_name, prop.name
741 )));
742 }
743 if key_seen {
744 return Err(NanoError::Parse(format!(
745 "property {}.{} declares @key multiple times",
746 type_name, prop.name
747 )));
748 }
749 key_seen = true;
750 }
751 "unique" => {
752 if ann.value.is_some() {
753 return Err(NanoError::Parse(format!(
754 "@unique on {}.{} does not accept a value",
755 type_name, prop.name
756 )));
757 }
758 if unique_seen {
759 return Err(NanoError::Parse(format!(
760 "property {}.{} declares @unique multiple times",
761 type_name, prop.name
762 )));
763 }
764 unique_seen = true;
765 }
766 "index" => {
767 if ann.value.is_some() {
768 return Err(NanoError::Parse(format!(
769 "@index on {}.{} does not accept a value",
770 type_name, prop.name
771 )));
772 }
773 if index_seen {
774 return Err(NanoError::Parse(format!(
775 "property {}.{} declares @index multiple times",
776 type_name, prop.name
777 )));
778 }
779 index_seen = true;
780 }
781 "embed" => {
782 if embed_seen {
783 return Err(NanoError::Parse(format!(
784 "property {}.{} declares @embed multiple times",
785 type_name, prop.name
786 )));
787 }
788 embed_seen = true;
789
790 if !is_vector {
791 return Err(NanoError::Parse(format!(
792 "@embed is only supported on vector properties ({}.{})",
793 type_name, prop.name
794 )));
795 }
796
797 let source_prop = ann.value.as_deref().ok_or_else(|| {
798 NanoError::Parse(format!(
799 "@embed on {}.{} requires a source property name",
800 type_name, prop.name
801 ))
802 })?;
803 if source_prop.trim().is_empty() {
804 return Err(NanoError::Parse(format!(
805 "@embed on {}.{} requires a non-empty source property name",
806 type_name, prop.name
807 )));
808 }
809
810 let source_decl = all_properties
811 .iter()
812 .find(|p| p.name == source_prop)
813 .ok_or_else(|| {
814 NanoError::Parse(format!(
815 "@embed on {}.{} references unknown source property {}",
816 type_name, prop.name, source_prop
817 ))
818 })?;
819 if source_decl.prop_type.list || source_decl.prop_type.scalar != ScalarType::String
820 {
821 return Err(NanoError::Parse(format!(
822 "@embed source property {}.{} must be String",
823 type_name, source_prop
824 )));
825 }
826 }
827 _ => {}
828 }
829 }
830 Ok(())
831}
832
833fn validate_constraints(schema: &SchemaFile) -> Result<()> {
836 for decl in &schema.declarations {
837 match decl {
838 SchemaDecl::Interface(_) => {}
839 SchemaDecl::Node(node) => {
840 validate_type_constraints(&node.constraints, &node.properties, &node.name, false)?;
841 }
842 SchemaDecl::Edge(edge) => {
843 validate_type_constraints(&edge.constraints, &edge.properties, &edge.name, true)?;
844 }
845 }
846 }
847 Ok(())
848}
849
850fn validate_type_constraints(
851 constraints: &[Constraint],
852 properties: &[PropDecl],
853 type_name: &str,
854 is_edge: bool,
855) -> Result<()> {
856 let prop_names: HashMap<&str, &PropDecl> =
857 properties.iter().map(|p| (p.name.as_str(), p)).collect();
858
859 let mut key_count = 0usize;
860
861 for constraint in constraints {
862 match constraint {
863 Constraint::Key(cols) => {
864 if is_edge {
865 return Err(NanoError::Parse(format!(
866 "@key constraint is not supported on edges (edge {})",
867 type_name
868 )));
869 }
870 key_count += 1;
871 if key_count > 1 {
872 return Err(NanoError::Parse(format!(
873 "node type {} has multiple @key constraints; only one is supported",
874 type_name
875 )));
876 }
877 for col in cols {
878 let prop = prop_names.get(col.as_str()).ok_or_else(|| {
879 NanoError::Parse(format!(
880 "@key on {} references unknown property '{}'",
881 type_name, col
882 ))
883 })?;
884 if prop.prop_type.nullable {
885 return Err(NanoError::Parse(format!(
886 "@key property {}.{} cannot be nullable",
887 type_name, col
888 )));
889 }
890 if prop.prop_type.list {
891 return Err(NanoError::Parse(format!(
892 "@key is not supported on list property {}.{}",
893 type_name, col
894 )));
895 }
896 if matches!(prop.prop_type.scalar, ScalarType::Vector(_)) {
897 return Err(NanoError::Parse(format!(
898 "@key is not supported on vector property {}.{}",
899 type_name, col
900 )));
901 }
902 if matches!(prop.prop_type.scalar, ScalarType::Blob) {
903 return Err(NanoError::Parse(format!(
904 "@key is not supported on blob property {}.{}",
905 type_name, col
906 )));
907 }
908 }
909 }
910 Constraint::Unique(cols) => {
911 for col in cols {
912 if is_edge && (col == "src" || col == "dst") {
914 continue;
915 }
916 if !prop_names.contains_key(col.as_str()) {
917 return Err(NanoError::Parse(format!(
918 "@unique on {} references unknown property '{}'",
919 type_name, col
920 )));
921 }
922 }
923 }
924 Constraint::Index(cols) => {
925 for col in cols {
926 if is_edge && (col == "src" || col == "dst") {
927 continue;
928 }
929 let prop = prop_names.get(col.as_str()).ok_or_else(|| {
930 NanoError::Parse(format!(
931 "@index on {} references unknown property '{}'",
932 type_name, col
933 ))
934 })?;
935 if matches!(prop.prop_type.scalar, ScalarType::Blob) {
936 return Err(NanoError::Parse(format!(
937 "@index is not supported on blob property {}.{}",
938 type_name, col
939 )));
940 }
941 }
942 }
943 Constraint::Range { property, .. } => {
944 if is_edge {
945 return Err(NanoError::Parse(format!(
946 "@range constraint is not supported on edges (edge {})",
947 type_name
948 )));
949 }
950 let prop = prop_names.get(property.as_str()).ok_or_else(|| {
951 NanoError::Parse(format!(
952 "@range on {} references unknown property '{}'",
953 type_name, property
954 ))
955 })?;
956 if !prop.prop_type.scalar.is_numeric() {
957 return Err(NanoError::Parse(format!(
958 "@range on {}.{} requires a numeric type, got {}",
959 type_name,
960 property,
961 prop.prop_type.display_name()
962 )));
963 }
964 }
965 Constraint::Check { property, .. } => {
966 if is_edge {
967 return Err(NanoError::Parse(format!(
968 "@check constraint is not supported on edges (edge {})",
969 type_name
970 )));
971 }
972 let prop = prop_names.get(property.as_str()).ok_or_else(|| {
973 NanoError::Parse(format!(
974 "@check on {} references unknown property '{}'",
975 type_name, property
976 ))
977 })?;
978 if prop.prop_type.scalar != ScalarType::String {
979 return Err(NanoError::Parse(format!(
980 "@check on {}.{} requires String type, got {}",
981 type_name,
982 property,
983 prop.prop_type.display_name()
984 )));
985 }
986 }
987 }
988 }
989
990 Ok(())
991}
992
993#[cfg(test)]
994#[path = "parser_tests.rs"]
995mod tests;