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)]
994mod tests {
995 use super::*;
996
997 #[test]
998 fn test_parse_basic_schema() {
999 let input = r#"
1000node Person {
1001 name: String
1002 age: I32?
1003}
1004
1005node Company {
1006 name: String
1007}
1008
1009edge Knows: Person -> Person {
1010 since: Date?
1011}
1012
1013edge WorksAt: Person -> Company {
1014 title: String?
1015}
1016"#;
1017 let schema = parse_schema(input).unwrap();
1018 assert_eq!(schema.declarations.len(), 4);
1019
1020 match &schema.declarations[0] {
1021 SchemaDecl::Node(n) => {
1022 assert_eq!(n.name, "Person");
1023 assert!(n.annotations.is_empty());
1024 assert!(n.implements.is_empty());
1025 assert_eq!(n.properties.len(), 2);
1026 assert_eq!(n.properties[0].name, "name");
1027 assert!(!n.properties[0].prop_type.nullable);
1028 assert_eq!(n.properties[1].name, "age");
1029 assert!(n.properties[1].prop_type.nullable);
1030 }
1031 _ => panic!("expected Node"),
1032 }
1033
1034 match &schema.declarations[2] {
1035 SchemaDecl::Edge(e) => {
1036 assert_eq!(e.name, "Knows");
1037 assert_eq!(e.from_type, "Person");
1038 assert_eq!(e.to_type, "Person");
1039 assert!(e.annotations.is_empty());
1040 assert_eq!(e.properties.len(), 1);
1041 assert!(e.cardinality.is_default());
1042 }
1043 _ => panic!("expected Edge"),
1044 }
1045 }
1046
1047 #[test]
1048 fn test_parse_interface_basic() {
1049 let input = r#"
1050interface Named {
1051 name: String
1052}
1053node Person implements Named {
1054 age: I32?
1055}
1056"#;
1057 let schema = parse_schema(input).unwrap();
1058 match &schema.declarations[0] {
1059 SchemaDecl::Interface(i) => {
1060 assert_eq!(i.name, "Named");
1061 assert_eq!(i.properties.len(), 1);
1062 assert_eq!(i.properties[0].name, "name");
1063 }
1064 _ => panic!("expected Interface"),
1065 }
1066 match &schema.declarations[1] {
1067 SchemaDecl::Node(n) => {
1068 assert_eq!(n.name, "Person");
1069 assert_eq!(n.implements, vec!["Named"]);
1070 assert_eq!(n.properties.len(), 2);
1072 }
1073 _ => panic!("expected Node"),
1074 }
1075 }
1076
1077 #[test]
1078 fn test_parse_implements_multiple() {
1079 let input = r#"
1080interface Slugged {
1081 slug: String @key
1082}
1083interface Described {
1084 title: String
1085 description: String?
1086}
1087node Signal implements Slugged, Described {
1088 strength: F64
1089}
1090"#;
1091 let schema = parse_schema(input).unwrap();
1092 match &schema.declarations[2] {
1093 SchemaDecl::Node(n) => {
1094 assert_eq!(n.name, "Signal");
1095 assert_eq!(n.implements, vec!["Slugged", "Described"]);
1096 assert_eq!(n.properties.len(), 4);
1098 assert!(
1100 n.constraints
1101 .iter()
1102 .any(|c| matches!(c, Constraint::Key(v) if v == &["slug"]))
1103 );
1104 }
1105 _ => panic!("expected Node"),
1106 }
1107 }
1108
1109 #[test]
1110 fn test_reject_implements_unknown_interface() {
1111 let input = r#"
1112node Person implements Unknown {
1113 name: String
1114}
1115"#;
1116 let err = parse_schema(input).unwrap_err();
1117 assert!(err.to_string().contains("unknown interface"));
1118 }
1119
1120 #[test]
1121 fn test_reject_interface_property_type_conflict() {
1122 let input = r#"
1123interface Named {
1124 name: I32
1125}
1126node Person implements Named {
1127 name: String
1128}
1129"#;
1130 let err = parse_schema(input).unwrap_err();
1131 assert!(err.to_string().contains("type") || err.to_string().contains("interface"));
1132 }
1133
1134 #[test]
1135 fn test_parse_annotation() {
1136 let input = r#"
1137node Person {
1138 name: String @unique
1139 id: U64 @key
1140 handle: String @index
1141}
1142"#;
1143 let schema = parse_schema(input).unwrap();
1144 match &schema.declarations[0] {
1145 SchemaDecl::Node(n) => {
1146 assert_eq!(n.properties[0].annotations.len(), 1);
1147 assert_eq!(n.properties[0].annotations[0].name, "unique");
1148 assert_eq!(n.properties[1].annotations[0].name, "key");
1149 assert_eq!(n.properties[2].annotations[0].name, "index");
1150 assert!(
1152 n.constraints
1153 .iter()
1154 .any(|c| matches!(c, Constraint::Unique(_)))
1155 );
1156 assert!(
1157 n.constraints
1158 .iter()
1159 .any(|c| matches!(c, Constraint::Key(_)))
1160 );
1161 assert!(
1162 n.constraints
1163 .iter()
1164 .any(|c| matches!(c, Constraint::Index(_)))
1165 );
1166 }
1167 _ => panic!("expected Node"),
1168 }
1169 }
1170
1171 #[test]
1172 fn test_property_level_key_desugars_to_constraint() {
1173 let input = r#"
1174node Person {
1175 name: String @key
1176}
1177"#;
1178 let schema = parse_schema(input).unwrap();
1179 match &schema.declarations[0] {
1180 SchemaDecl::Node(n) => {
1181 assert!(
1182 n.constraints
1183 .iter()
1184 .any(|c| matches!(c, Constraint::Key(v) if v == &["name"]))
1185 );
1186 }
1187 _ => panic!("expected Node"),
1188 }
1189 }
1190
1191 #[test]
1192 fn test_parse_body_constraint_key() {
1193 let input = r#"
1194node Person {
1195 name: String
1196 @key(name)
1197}
1198"#;
1199 let schema = parse_schema(input).unwrap();
1200 match &schema.declarations[0] {
1201 SchemaDecl::Node(n) => {
1202 assert!(
1203 n.constraints
1204 .iter()
1205 .any(|c| matches!(c, Constraint::Key(v) if v == &["name"]))
1206 );
1207 }
1208 _ => panic!("expected Node"),
1209 }
1210 }
1211
1212 #[test]
1213 fn test_parse_body_constraint_unique_composite() {
1214 let input = r#"
1215node Person {
1216 first: String
1217 last: String
1218 @unique(first, last)
1219}
1220"#;
1221 let schema = parse_schema(input).unwrap();
1222 match &schema.declarations[0] {
1223 SchemaDecl::Node(n) => {
1224 assert!(
1225 n.constraints
1226 .iter()
1227 .any(|c| matches!(c, Constraint::Unique(v) if v == &["first", "last"]))
1228 );
1229 }
1230 _ => panic!("expected Node"),
1231 }
1232 }
1233
1234 #[test]
1235 fn test_parse_body_constraint_index_composite() {
1236 let input = r#"
1237node Event {
1238 category: String
1239 date: Date
1240 @index(category, date)
1241}
1242"#;
1243 let schema = parse_schema(input).unwrap();
1244 match &schema.declarations[0] {
1245 SchemaDecl::Node(n) => {
1246 assert!(
1247 n.constraints
1248 .iter()
1249 .any(|c| matches!(c, Constraint::Index(v) if v == &["category", "date"]))
1250 );
1251 }
1252 _ => panic!("expected Node"),
1253 }
1254 }
1255
1256 #[test]
1257 fn test_parse_body_constraint_range() {
1258 let input = r#"
1259node Person {
1260 age: I32?
1261 @range(age, 0..200)
1262}
1263"#;
1264 let schema = parse_schema(input).unwrap();
1265 match &schema.declarations[0] {
1266 SchemaDecl::Node(n) => {
1267 assert!(
1268 n.constraints.iter().any(
1269 |c| matches!(c, Constraint::Range { property, .. } if property == "age")
1270 )
1271 );
1272 }
1273 _ => panic!("expected Node"),
1274 }
1275 }
1276
1277 #[test]
1278 fn test_parse_range_float_bounds() {
1279 let input = r#"
1280node Measurement {
1281 name: String @key
1282 temperature: F64?
1283 @range(temperature, 0.0..100.0)
1284}
1285"#;
1286 let schema = parse_schema(input).unwrap();
1287 match &schema.declarations[0] {
1288 SchemaDecl::Node(n) => {
1289 assert!(n.constraints.iter().any(|c| matches!(
1290 c,
1291 Constraint::Range { property, min, max }
1292 if property == "temperature"
1293 && matches!(min, Some(ConstraintBound::Float(f)) if *f == 0.0)
1294 && matches!(max, Some(ConstraintBound::Float(f)) if *f == 100.0)
1295 )));
1296 }
1297 _ => panic!("expected Node"),
1298 }
1299 }
1300
1301 #[test]
1302 fn test_parse_range_negative_float_bounds() {
1303 let input = r#"
1304node Measurement {
1305 name: String @key
1306 temperature: F64?
1307 @range(temperature, -40.0..60.0)
1308}
1309"#;
1310 let schema = parse_schema(input).unwrap();
1311 match &schema.declarations[0] {
1312 SchemaDecl::Node(n) => {
1313 assert!(n.constraints.iter().any(|c| matches!(
1314 c,
1315 Constraint::Range { property, min, max }
1316 if property == "temperature"
1317 && matches!(min, Some(ConstraintBound::Float(f)) if *f == -40.0)
1318 && matches!(max, Some(ConstraintBound::Float(f)) if *f == 60.0)
1319 )));
1320 }
1321 _ => panic!("expected Node"),
1322 }
1323 }
1324
1325 #[test]
1326 fn test_parse_range_negative_integer_bounds() {
1327 let input = r#"
1328node Account {
1329 name: String @key
1330 balance: I64?
1331 @range(balance, -1000..1000)
1332}
1333"#;
1334 let schema = parse_schema(input).unwrap();
1335 match &schema.declarations[0] {
1336 SchemaDecl::Node(n) => {
1337 assert!(n.constraints.iter().any(|c| matches!(
1338 c,
1339 Constraint::Range { property, min, max }
1340 if property == "balance"
1341 && matches!(min, Some(ConstraintBound::Integer(n)) if *n == -1000)
1342 && matches!(max, Some(ConstraintBound::Integer(n)) if *n == 1000)
1343 )));
1344 }
1345 _ => panic!("expected Node"),
1346 }
1347 }
1348
1349 #[test]
1350 fn test_parse_body_constraint_check() {
1351 let input = r#"
1352node Order {
1353 code: String
1354 @check(code, "[A-Z]{3}-[0-9]+")
1355}
1356"#;
1357 let schema = parse_schema(input).unwrap();
1358 match &schema.declarations[0] {
1359 SchemaDecl::Node(n) => {
1360 assert!(n.constraints.iter().any(|c| matches!(c, Constraint::Check { property, pattern } if property == "code" && pattern == "[A-Z]{3}-[0-9]+")));
1361 }
1362 _ => panic!("expected Node"),
1363 }
1364 }
1365
1366 #[test]
1367 fn test_reject_range_on_string() {
1368 let input = r#"
1369node Person {
1370 name: String
1371 @range(name, 0..100)
1372}
1373"#;
1374 let err = parse_schema(input).unwrap_err();
1375 assert!(err.to_string().contains("numeric"));
1376 }
1377
1378 #[test]
1379 fn test_reject_check_on_integer() {
1380 let input = r#"
1381node Person {
1382 age: I32
1383 @check(age, "[0-9]+")
1384}
1385"#;
1386 let err = parse_schema(input).unwrap_err();
1387 assert!(err.to_string().contains("String"));
1388 }
1389
1390 #[test]
1391 fn test_parse_edge_cardinality() {
1392 let input = r#"
1393node Person { name: String }
1394node Company { name: String }
1395edge WorksAt: Person -> Company @card(0..1)
1396"#;
1397 let schema = parse_schema(input).unwrap();
1398 match &schema.declarations[2] {
1399 SchemaDecl::Edge(e) => {
1400 assert_eq!(e.cardinality.min, 0);
1401 assert_eq!(e.cardinality.max, Some(1));
1402 }
1403 _ => panic!("expected Edge"),
1404 }
1405 }
1406
1407 #[test]
1408 fn test_parse_edge_cardinality_unbounded() {
1409 let input = r#"
1410node Person { name: String }
1411node Paper { title: String }
1412edge Authored: Person -> Paper @card(1..)
1413"#;
1414 let schema = parse_schema(input).unwrap();
1415 match &schema.declarations[2] {
1416 SchemaDecl::Edge(e) => {
1417 assert_eq!(e.cardinality.min, 1);
1418 assert_eq!(e.cardinality.max, None);
1419 }
1420 _ => panic!("expected Edge"),
1421 }
1422 }
1423
1424 #[test]
1425 fn test_parse_edge_default_cardinality() {
1426 let input = r#"
1427node Person { name: String }
1428edge Knows: Person -> Person
1429"#;
1430 let schema = parse_schema(input).unwrap();
1431 match &schema.declarations[1] {
1432 SchemaDecl::Edge(e) => {
1433 assert!(e.cardinality.is_default());
1434 }
1435 _ => panic!("expected Edge"),
1436 }
1437 }
1438
1439 #[test]
1440 fn test_parse_edge_unique_src_dst() {
1441 let input = r#"
1442node Person { name: String }
1443edge Knows: Person -> Person {
1444 @unique(src, dst)
1445}
1446"#;
1447 let schema = parse_schema(input).unwrap();
1448 match &schema.declarations[1] {
1449 SchemaDecl::Edge(e) => {
1450 assert!(
1451 e.constraints
1452 .iter()
1453 .any(|c| matches!(c, Constraint::Unique(v) if v == &["src", "dst"]))
1454 );
1455 }
1456 _ => panic!("expected Edge"),
1457 }
1458 }
1459
1460 #[test]
1461 fn test_parse_edge_property_index() {
1462 let input = r#"
1463node Person { name: String }
1464node Company { name: String }
1465edge WorksAt: Person -> Company {
1466 since: Date? @index
1467}
1468"#;
1469 let schema = parse_schema(input).unwrap();
1470 match &schema.declarations[2] {
1471 SchemaDecl::Edge(e) => {
1472 assert!(
1474 e.constraints
1475 .iter()
1476 .any(|c| matches!(c, Constraint::Index(v) if v == &["since"]))
1477 );
1478 }
1479 _ => panic!("expected Edge"),
1480 }
1481 }
1482
1483 #[test]
1484 fn test_parse_embed_annotation_identifier_arg() {
1485 let input = r#"
1486node Doc {
1487 title: String
1488 embedding: Vector(3) @embed(title)
1489}
1490"#;
1491 let schema = parse_schema(input).unwrap();
1492 match &schema.declarations[0] {
1493 SchemaDecl::Node(n) => {
1494 assert_eq!(n.properties[1].annotations.len(), 1);
1495 assert_eq!(n.properties[1].annotations[0].name, "embed");
1496 assert_eq!(
1497 n.properties[1].annotations[0].value.as_deref(),
1498 Some("title")
1499 );
1500 }
1501 _ => panic!("expected Node"),
1502 }
1503 }
1504
1505 #[test]
1506 fn test_parse_edge_no_body() {
1507 let input = "edge WorksAt: Person -> Company\n";
1508 let schema = parse_schema(input).unwrap();
1509 match &schema.declarations[0] {
1510 SchemaDecl::Edge(e) => {
1511 assert_eq!(e.name, "WorksAt");
1512 assert!(e.annotations.is_empty());
1513 assert!(e.properties.is_empty());
1514 }
1515 _ => panic!("expected Edge"),
1516 }
1517 }
1518
1519 #[test]
1520 fn test_parse_type_rename_annotation() {
1521 let input = r#"
1522node Account @rename_from("User") {
1523 full_name: String @rename_from("name")
1524}
1525
1526edge ConnectedTo: Account -> Account @rename_from("Knows")
1527"#;
1528 let schema = parse_schema(input).unwrap();
1529 match &schema.declarations[0] {
1530 SchemaDecl::Node(n) => {
1531 assert_eq!(n.name, "Account");
1532 assert_eq!(n.annotations.len(), 1);
1533 assert_eq!(n.annotations[0].name, "rename_from");
1534 assert_eq!(n.annotations[0].value.as_deref(), Some("User"));
1535 assert_eq!(n.properties[0].annotations[0].name, "rename_from");
1536 assert_eq!(
1537 n.properties[0].annotations[0].value.as_deref(),
1538 Some("name")
1539 );
1540 }
1541 _ => panic!("expected Node"),
1542 }
1543 match &schema.declarations[1] {
1544 SchemaDecl::Edge(e) => {
1545 assert_eq!(e.name, "ConnectedTo");
1546 assert_eq!(e.annotations.len(), 1);
1547 assert_eq!(e.annotations[0].name, "rename_from");
1548 assert_eq!(e.annotations[0].value.as_deref(), Some("Knows"));
1549 }
1550 _ => panic!("expected Edge"),
1551 }
1552 }
1553
1554 #[test]
1555 fn test_reject_multiple_node_keys() {
1556 let input = r#"
1557node Person {
1558 id: U64 @key
1559 ext_id: String @key
1560}
1561"#;
1562 let err = parse_schema(input).unwrap_err();
1563 assert!(err.to_string().contains("multiple @key"));
1564 }
1565
1566 #[test]
1567 fn test_reject_unique_with_value() {
1568 let input = r#"
1571node Person {
1572 email: String @unique("x")
1573}
1574"#;
1575 assert!(parse_schema(input).is_err());
1576 }
1577
1578 #[test]
1579 fn test_reject_index_with_value() {
1580 let input = r#"
1582node Person {
1583 email: String @index("x")
1584}
1585"#;
1586 assert!(parse_schema(input).is_err());
1587 }
1588
1589 #[test]
1590 fn test_reject_unique_on_node_annotation() {
1591 let input = r#"
1592node Person @unique {
1593 email: String
1594}
1595"#;
1596 let err = parse_schema(input).unwrap_err();
1597 assert!(
1598 err.to_string()
1599 .contains("only supported on node properties")
1600 );
1601 }
1602
1603 #[test]
1604 fn test_reject_index_on_node_annotation() {
1605 let input = r#"
1606node Person @index {
1607 email: String
1608}
1609"#;
1610 let err = parse_schema(input).unwrap_err();
1611 assert!(
1612 err.to_string()
1613 .contains("only supported on node properties")
1614 );
1615 }
1616
1617 #[test]
1618 fn test_allow_unique_on_edge_property() {
1619 let input = r#"
1620node Person { name: String }
1621edge Knows: Person -> Person {
1622 weight: I32 @unique
1623}
1624"#;
1625 let schema = parse_schema(input).unwrap();
1627 match &schema.declarations[1] {
1628 SchemaDecl::Edge(e) => {
1629 assert!(
1630 e.constraints
1631 .iter()
1632 .any(|c| matches!(c, Constraint::Unique(v) if v == &["weight"]))
1633 );
1634 }
1635 _ => panic!("expected Edge"),
1636 }
1637 }
1638
1639 #[test]
1640 fn test_allow_index_on_edge_property() {
1641 let input = r#"
1642node Person { name: String }
1643edge Knows: Person -> Person {
1644 weight: I32 @index
1645}
1646"#;
1647 let schema = parse_schema(input).unwrap();
1649 match &schema.declarations[1] {
1650 SchemaDecl::Edge(e) => {
1651 assert!(
1652 e.constraints
1653 .iter()
1654 .any(|c| matches!(c, Constraint::Index(v) if v == &["weight"]))
1655 );
1656 }
1657 _ => panic!("expected Edge"),
1658 }
1659 }
1660
1661 #[test]
1662 fn test_reject_embed_without_source_property() {
1663 let input = r#"
1664node Doc {
1665 title: String
1666 embedding: Vector(3) @embed
1667}
1668"#;
1669 let err = parse_schema(input).unwrap_err();
1670 assert!(err.to_string().contains("requires a source property name"));
1671 }
1672
1673 #[test]
1674 fn test_reject_embed_on_non_vector_property() {
1675 let input = r#"
1676node Doc {
1677 title: String @embed(title)
1678}
1679"#;
1680 let err = parse_schema(input).unwrap_err();
1681 assert!(
1682 err.to_string()
1683 .contains("only supported on vector properties")
1684 );
1685 }
1686
1687 #[test]
1688 fn test_reject_embed_unknown_source_property() {
1689 let input = r#"
1690node Doc {
1691 title: String
1692 embedding: Vector(3) @embed(body)
1693}
1694"#;
1695 let err = parse_schema(input).unwrap_err();
1696 assert!(
1697 err.to_string()
1698 .contains("references unknown source property")
1699 );
1700 }
1701
1702 #[test]
1703 fn test_reject_embed_source_not_string() {
1704 let input = r#"
1705node Doc {
1706 body: I32
1707 embedding: Vector(3) @embed(body)
1708}
1709"#;
1710 let err = parse_schema(input).unwrap_err();
1711 assert!(err.to_string().contains("must be String"));
1712 }
1713
1714 #[test]
1715 fn test_reject_embed_on_edge_property() {
1716 let input = r#"
1717node Doc { title: String }
1718edge Linked: Doc -> Doc {
1719 embedding: Vector(3) @embed(title)
1720}
1721"#;
1722 let err = parse_schema(input).unwrap_err();
1723 assert!(err.to_string().contains("edge properties"));
1724 }
1725
1726 #[test]
1727 fn test_parse_enum_and_list_types() {
1728 let input = r#"
1729node Ticket {
1730 status: enum(open, closed, blocked)
1731 tags: [String]
1732}
1733"#;
1734 let schema = parse_schema(input).unwrap();
1735 match &schema.declarations[0] {
1736 SchemaDecl::Node(n) => {
1737 let status = &n.properties[0].prop_type;
1738 assert!(status.is_enum());
1739 assert!(!status.list);
1740 assert_eq!(
1741 status.enum_values.as_ref().unwrap(),
1742 &vec![
1743 "blocked".to_string(),
1744 "closed".to_string(),
1745 "open".to_string()
1746 ]
1747 );
1748
1749 let tags = &n.properties[1].prop_type;
1750 assert!(tags.list);
1751 assert!(!tags.is_enum());
1752 assert_eq!(tags.scalar, ScalarType::String);
1753 }
1754 _ => panic!("expected Node"),
1755 }
1756 }
1757
1758 #[test]
1759 fn test_reject_duplicate_enum_values() {
1760 let input = r#"
1761node Ticket {
1762 status: enum(open, closed, open)
1763}
1764"#;
1765 let err = parse_schema(input).unwrap_err();
1766 assert!(err.to_string().contains("duplicate values"));
1767 }
1768
1769 #[test]
1770 fn test_parse_description_and_instruction_annotations() {
1771 let input = r#"
1772node Task @description("Tracked work item") @instruction("Prefer querying by slug") {
1773 slug: String @key @description("Stable external identifier")
1774}
1775edge DependsOn: Task -> Task @description("Hard dependency") @instruction("Use only for blockers")
1776"#;
1777 let schema = parse_schema(input).unwrap();
1778 match &schema.declarations[0] {
1779 SchemaDecl::Node(node) => {
1780 assert_eq!(
1781 node.annotations
1782 .iter()
1783 .find(|ann| ann.name == "description")
1784 .and_then(|ann| ann.value.as_deref()),
1785 Some("Tracked work item")
1786 );
1787 assert_eq!(
1788 node.annotations
1789 .iter()
1790 .find(|ann| ann.name == "instruction")
1791 .and_then(|ann| ann.value.as_deref()),
1792 Some("Prefer querying by slug")
1793 );
1794 assert_eq!(
1795 node.properties[0]
1796 .annotations
1797 .iter()
1798 .find(|ann| ann.name == "description")
1799 .and_then(|ann| ann.value.as_deref()),
1800 Some("Stable external identifier")
1801 );
1802 }
1803 _ => panic!("expected node"),
1804 }
1805 match &schema.declarations[1] {
1806 SchemaDecl::Edge(edge) => {
1807 assert_eq!(
1808 edge.annotations
1809 .iter()
1810 .find(|ann| ann.name == "description")
1811 .and_then(|ann| ann.value.as_deref()),
1812 Some("Hard dependency")
1813 );
1814 assert_eq!(
1815 edge.annotations
1816 .iter()
1817 .find(|ann| ann.name == "instruction")
1818 .and_then(|ann| ann.value.as_deref()),
1819 Some("Use only for blockers")
1820 );
1821 }
1822 _ => panic!("expected edge"),
1823 }
1824 }
1825
1826 #[test]
1827 fn test_parse_annotation_decodes_escapes() {
1828 let input = r#"
1829node Task @description("Tracked\n\"work\"\\item") {
1830 slug: String @key @description("Stable\tidentifier")
1831}
1832"#;
1833 let schema = parse_schema(input).unwrap();
1834 match &schema.declarations[0] {
1835 SchemaDecl::Node(node) => {
1836 assert_eq!(
1837 node.annotations[0].value.as_deref(),
1838 Some("Tracked\n\"work\"\\item")
1839 );
1840 assert_eq!(
1841 node.properties[0].annotations[1].value.as_deref(),
1842 Some("Stable\tidentifier")
1843 );
1844 }
1845 _ => panic!("expected node"),
1846 }
1847 }
1848
1849 #[test]
1850 fn test_parse_annotation_rejects_unknown_escape() {
1851 let input = r#"
1852node Task @description("Tracked\q") {
1853 slug: String @key
1854}
1855"#;
1856 let err = parse_schema(input).unwrap_err();
1857 assert!(err.to_string().contains("unsupported escape sequence"));
1858 }
1859
1860 #[test]
1861 fn test_reject_duplicate_description_annotations() {
1862 let input = r#"
1863node Task @description("a") @description("b") {
1864 slug: String @key
1865}
1866"#;
1867 let err = parse_schema(input).unwrap_err();
1868 assert!(
1869 err.to_string()
1870 .contains("declares @description multiple times")
1871 );
1872 }
1873
1874 #[test]
1875 fn test_reject_instruction_on_property() {
1876 let input = r#"
1877node Task {
1878 slug: String @instruction("bad")
1879}
1880"#;
1881 let err = parse_schema(input).unwrap_err();
1882 assert!(
1883 err.to_string()
1884 .contains("@instruction is only supported on node and edge types")
1885 );
1886 }
1887
1888 #[test]
1889 fn test_reject_key_on_list_property() {
1890 let input = r#"
1891node Ticket {
1892 tags: [String] @key
1893}
1894"#;
1895 let err = parse_schema(input).unwrap_err();
1896 assert!(err.to_string().contains("list property"));
1897 }
1898
1899 #[test]
1900 fn test_parse_vector_type() {
1901 let input = r#"
1902node Doc {
1903 embedding: Vector(3)
1904}
1905"#;
1906 let schema = parse_schema(input).unwrap();
1907 match &schema.declarations[0] {
1908 SchemaDecl::Node(n) => match n.properties[0].prop_type.scalar {
1909 ScalarType::Vector(dim) => assert_eq!(dim, 3),
1910 other => panic!("expected vector type, got {:?}", other),
1911 },
1912 _ => panic!("expected node"),
1913 }
1914 }
1915
1916 #[test]
1917 fn test_reject_zero_vector_dimension() {
1918 let input = r#"
1919node Doc {
1920 embedding: Vector(0)
1921}
1922"#;
1923 let err = parse_schema(input).unwrap_err();
1924 assert!(err.to_string().contains("Vector dimension"));
1925 }
1926
1927 #[test]
1928 fn test_reject_vector_dimension_larger_than_arrow_bound() {
1929 let input = r#"
1930node Doc {
1931 embedding: Vector(2147483648)
1932}
1933"#;
1934 let err = parse_schema(input).unwrap_err();
1935 assert!(err.to_string().contains("exceeds maximum supported"));
1936 }
1937
1938 #[test]
1939 fn test_parse_error() {
1940 let input = "node { }"; assert!(parse_schema(input).is_err());
1942 }
1943
1944 #[test]
1945 fn test_parse_error_diagnostic_has_span() {
1946 let input = "node { }";
1947 let err = parse_schema_diagnostic(input).unwrap_err();
1948 assert!(err.span.is_some());
1949 }
1950}