Skip to main content

omnigraph_compiler/schema/
parser.rs

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    // Collect interfaces for resolution (clone to avoid borrow conflict)
38    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    // Resolve implements clauses on nodes
47    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-level @key/@unique/@index annotations into constraints
131    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-level @unique/@index on edge properties
166    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            // @range(prop, min..max)
248            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            // The second arg should be a range_bound
255            let (min, max) = extract_range_bounds(&args[1])?;
256            Ok(Constraint::Range { property, min, max })
257        }
258        "check" => {
259            // @check(prop, "regex")
260            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    // constraint_arg wraps ident or literal
282    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    // Navigate into constraint_arg -> literal -> string_lit
302    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    // Find the range_bound node inside the constraint_arg
322    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    // Try as integer first
367    if let Ok(n) = text.parse::<i64>() {
368        return Ok(ConstraintBound::Integer(n));
369    }
370    // Try as float
371    if let Ok(f) = text.parse::<f64>() {
372        return Ok(ConstraintBound::Float(f));
373    }
374
375    // Navigate into literal -> integer/float_lit
376    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
392/// Desugar property-level @key/@unique/@index annotations into body-level constraints.
393fn 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
412/// Resolve interface implements clauses — verify properties exist or inject them.
413fn 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                // Property exists — verify type compatibility
428                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                // Property missing — inject it from the interface
440                node.properties.push(iface_prop.clone());
441                // Also desugar any constraint annotations from the injected property
442                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        // `annotation_args`: one positional arg followed by zero or more
566        // `key = literal` kwargs (e.g. `@embed("source", model="…")`).
567        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
623// ─── Annotation Validation (metadata only) ───────────────────────────────────
624
625fn validate_schema_annotations(schema: &SchemaFile) -> Result<()> {
626    for decl in &schema.declarations {
627        match decl {
628            SchemaDecl::Interface(_) => {} // Interfaces have no type-level annotations
629            SchemaDecl::Node(node) => {
630                // Reject constraint annotations on node level (must be on properties or as body constraints)
631                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                // Validate property-level annotations
655                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        // List/vector/blob restrictions on property-level annotations
714        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        // Edge-specific restrictions
750        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        // Arity checks
758        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                // `model` is the only supported kwarg; reject the rest loudly so
851                // a typo can't be silently ignored (it would never validate).
852                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
867// ─── Constraint Validation ───────────────────────────────────────────────────
868
869fn 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                    // Allow "src" and "dst" as implicit edge columns
947                    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;