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