Skip to main content

prax_schema/parser/
mod.rs

1//! Schema parser for `.prax` files.
2
3mod grammar;
4
5use std::path::Path;
6
7use pest::Parser;
8use smol_str::SmolStr;
9use tracing::{debug, info};
10
11use crate::ast::*;
12use crate::error::{SchemaError, SchemaResult};
13
14pub use grammar::{PraxParser, Rule};
15
16use crate::ast::{
17    MssqlBlockOperation, Policy, PolicyCommand, PolicyType, Server, ServerGroup, ServerProperty,
18    ServerPropertyValue,
19};
20
21/// Escape a raw value for embedding in a `.prax` string literal (`"..."`).
22///
23/// Mirrors the grammar's `string_content` rule: only `"` and `\` need
24/// escaping. Writers (schema generators, `Display` impls) must use this so
25/// arbitrary text — e.g. a MySQL enum value containing a quote — survives a
26/// write→parse round-trip instead of producing an unparseable file.
27pub fn escape_prax_string(raw: &str) -> String {
28    raw.replace('\\', "\\\\").replace('"', "\\\"")
29}
30
31/// Unescape the inner content of a `string_literal` (quotes already
32/// stripped). Only `\"` → `"` and `\\` → `\` are interpreted; any other
33/// `\x` keeps its backslash so schemas written before escape support parse
34/// byte-identically.
35pub fn unescape_prax_string(inner: &str) -> String {
36    let mut out = String::with_capacity(inner.len());
37    let mut chars = inner.chars();
38    while let Some(c) = chars.next() {
39        if c != '\\' {
40            out.push(c);
41            continue;
42        }
43        match chars.next() {
44            Some('"') => out.push('"'),
45            Some('\\') => out.push('\\'),
46            Some(other) => {
47                out.push('\\');
48                out.push(other);
49            }
50            None => out.push('\\'),
51        }
52    }
53    out
54}
55
56/// Strip the surrounding double quotes of a `string_literal` pair's text
57/// and unescape its content (see [`unescape_prax_string`]).
58fn unquote_string_literal(s: &str) -> String {
59    unescape_prax_string(&s[1..s.len() - 1])
60}
61
62/// Parse a schema from a string.
63pub fn parse_schema(input: &str) -> SchemaResult<Schema> {
64    debug!(input_len = input.len(), "parse_schema() starting");
65    let pairs = PraxParser::parse(Rule::schema, input)
66        .map_err(|e| SchemaError::syntax(input.to_string(), 0, input.len(), e.to_string()))?;
67
68    let mut schema = Schema::new();
69    let mut current_doc: Option<Documentation> = None;
70
71    // The top-level parse result contains a single "schema" rule - get its inner pairs
72    let schema_pair = pairs.into_iter().next().unwrap();
73
74    for pair in schema_pair.into_inner() {
75        match pair.as_rule() {
76            Rule::documentation => {
77                let span = pair.as_span();
78                let text = pair
79                    .into_inner()
80                    .map(|p| p.as_str().trim_start_matches("///").trim())
81                    .collect::<Vec<_>>()
82                    .join("\n");
83                current_doc = Some(Documentation::new(
84                    text,
85                    Span::new(span.start(), span.end()),
86                ));
87            }
88            Rule::model_def => {
89                let mut model = parse_model(pair)?;
90                if let Some(doc) = current_doc.take() {
91                    model = model.with_documentation(doc);
92                }
93                schema.add_model(model);
94            }
95            Rule::enum_def => {
96                let mut e = parse_enum(pair)?;
97                if let Some(doc) = current_doc.take() {
98                    e = e.with_documentation(doc);
99                }
100                schema.add_enum(e);
101            }
102            Rule::type_def => {
103                let mut t = parse_composite_type(pair)?;
104                if let Some(doc) = current_doc.take() {
105                    t = t.with_documentation(doc);
106                }
107                schema.add_type(t);
108            }
109            Rule::view_def => {
110                let mut v = parse_view(pair)?;
111                if let Some(doc) = current_doc.take() {
112                    v = v.with_documentation(doc);
113                }
114                schema.add_view(v);
115            }
116            Rule::raw_sql_def => {
117                let sql = parse_raw_sql(pair)?;
118                schema.add_raw_sql(sql);
119            }
120            Rule::server_group_def => {
121                let mut sg = parse_server_group(pair)?;
122                if let Some(doc) = current_doc.take() {
123                    sg.set_documentation(doc);
124                }
125                schema.add_server_group(sg);
126            }
127            Rule::policy_def => {
128                let mut policy = parse_policy(pair)?;
129                if let Some(doc) = current_doc.take() {
130                    policy = policy.with_documentation(doc);
131                }
132                schema.add_policy(policy);
133            }
134            Rule::datasource_def => {
135                let ds = parse_datasource(pair)?;
136                schema.set_datasource(ds);
137                current_doc = None;
138            }
139            Rule::generator_def => {
140                let generator = parse_generator(pair)?;
141                schema.add_generator(generator);
142                current_doc = None;
143            }
144            Rule::EOI => {}
145            _ => {}
146        }
147    }
148
149    info!(
150        models = schema.models.len(),
151        enums = schema.enums.len(),
152        types = schema.types.len(),
153        views = schema.views.len(),
154        generators = schema.generators.len(),
155        policies = schema.policies.len(),
156        "Schema parsed successfully"
157    );
158    Ok(schema)
159}
160
161/// Parse a schema from a file.
162pub fn parse_schema_file(path: impl AsRef<Path>) -> SchemaResult<Schema> {
163    let path = path.as_ref();
164    info!(path = %path.display(), "Loading schema file");
165    let content = std::fs::read_to_string(path).map_err(|e| SchemaError::IoError {
166        path: path.display().to_string(),
167        source: e,
168    })?;
169
170    parse_schema(&content)
171}
172
173/// Parse a model definition.
174fn parse_model(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<Model> {
175    let span = pair.as_span();
176    let mut inner = pair.into_inner();
177
178    let name_pair = inner.next().unwrap();
179    let name = Ident::new(
180        name_pair.as_str(),
181        Span::new(name_pair.as_span().start(), name_pair.as_span().end()),
182    );
183
184    let mut model = Model::new(name, Span::new(span.start(), span.end()));
185
186    for item in inner {
187        match item.as_rule() {
188            Rule::field_def => {
189                let field = parse_field(item)?;
190                model.add_field(field);
191            }
192            Rule::model_attribute => {
193                let attr = parse_attribute(item)?;
194                model.attributes.push(attr);
195            }
196            Rule::model_body_item => {
197                // Unwrap the model_body_item to get the actual field_def or model_attribute
198                let inner_item = item.into_inner().next().unwrap();
199                match inner_item.as_rule() {
200                    Rule::field_def => {
201                        let field = parse_field(inner_item)?;
202                        model.add_field(field);
203                    }
204                    Rule::model_attribute => {
205                        let attr = parse_attribute(inner_item)?;
206                        model.attributes.push(attr);
207                    }
208                    _ => {}
209                }
210            }
211            _ => {}
212        }
213    }
214
215    Ok(model)
216}
217
218/// Parse an enum definition.
219fn parse_enum(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<Enum> {
220    let span = pair.as_span();
221    let mut inner = pair.into_inner();
222
223    let name_pair = inner.next().unwrap();
224    let name = Ident::new(
225        name_pair.as_str(),
226        Span::new(name_pair.as_span().start(), name_pair.as_span().end()),
227    );
228
229    let mut e = Enum::new(name, Span::new(span.start(), span.end()));
230
231    for item in inner {
232        match item.as_rule() {
233            Rule::enum_variant => {
234                let variant = parse_enum_variant(item)?;
235                e.add_variant(variant);
236            }
237            Rule::model_attribute => {
238                let attr = parse_attribute(item)?;
239                e.attributes.push(attr);
240            }
241            Rule::enum_body_item => {
242                // Unwrap the enum_body_item to get the actual enum_variant or model_attribute
243                let inner_item = item.into_inner().next().unwrap();
244                match inner_item.as_rule() {
245                    Rule::enum_variant => {
246                        let variant = parse_enum_variant(inner_item)?;
247                        e.add_variant(variant);
248                    }
249                    Rule::model_attribute => {
250                        let attr = parse_attribute(inner_item)?;
251                        e.attributes.push(attr);
252                    }
253                    _ => {}
254                }
255            }
256            _ => {}
257        }
258    }
259
260    Ok(e)
261}
262
263/// Parse an enum variant.
264fn parse_enum_variant(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<EnumVariant> {
265    let span = pair.as_span();
266    let mut inner = pair.into_inner();
267
268    let name_pair = inner.next().unwrap();
269    let name = Ident::new(
270        name_pair.as_str(),
271        Span::new(name_pair.as_span().start(), name_pair.as_span().end()),
272    );
273
274    let mut variant = EnumVariant::new(name, Span::new(span.start(), span.end()));
275
276    for item in inner {
277        if item.as_rule() == Rule::field_attribute {
278            let attr = parse_attribute(item)?;
279            variant.attributes.push(attr);
280        }
281    }
282
283    Ok(variant)
284}
285
286/// Parse a composite type definition.
287fn parse_composite_type(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<CompositeType> {
288    let span = pair.as_span();
289    let mut inner = pair.into_inner();
290
291    let name_pair = inner.next().unwrap();
292    let name = Ident::new(
293        name_pair.as_str(),
294        Span::new(name_pair.as_span().start(), name_pair.as_span().end()),
295    );
296
297    let mut t = CompositeType::new(name, Span::new(span.start(), span.end()));
298
299    for item in inner {
300        if item.as_rule() == Rule::field_def {
301            let field = parse_field(item)?;
302            t.add_field(field);
303        }
304    }
305
306    Ok(t)
307}
308
309/// Parse a view definition.
310fn parse_view(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<View> {
311    let span = pair.as_span();
312    let mut inner = pair.into_inner();
313
314    let name_pair = inner.next().unwrap();
315    let name = Ident::new(
316        name_pair.as_str(),
317        Span::new(name_pair.as_span().start(), name_pair.as_span().end()),
318    );
319
320    let mut v = View::new(name, Span::new(span.start(), span.end()));
321
322    for item in inner {
323        match item.as_rule() {
324            Rule::field_def => {
325                let field = parse_field(item)?;
326                v.add_field(field);
327            }
328            Rule::model_attribute => {
329                let attr = parse_attribute(item)?;
330                v.attributes.push(attr);
331            }
332            Rule::model_body_item => {
333                // Unwrap the model_body_item to get the actual field_def or model_attribute
334                let inner_item = item.into_inner().next().unwrap();
335                match inner_item.as_rule() {
336                    Rule::field_def => {
337                        let field = parse_field(inner_item)?;
338                        v.add_field(field);
339                    }
340                    Rule::model_attribute => {
341                        let attr = parse_attribute(inner_item)?;
342                        v.attributes.push(attr);
343                    }
344                    _ => {}
345                }
346            }
347            _ => {}
348        }
349    }
350
351    Ok(v)
352}
353
354/// Parse a field definition.
355fn parse_field(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<Field> {
356    let span = pair.as_span();
357    let mut inner = pair.into_inner();
358
359    let name_pair = inner.next().unwrap();
360    let name = Ident::new(
361        name_pair.as_str(),
362        Span::new(name_pair.as_span().start(), name_pair.as_span().end()),
363    );
364
365    let type_pair = inner.next().unwrap();
366    let (field_type, modifier) = parse_field_type(type_pair)?;
367
368    let mut attributes = vec![];
369    for item in inner {
370        if item.as_rule() == Rule::field_attribute {
371            let attr = parse_attribute(item)?;
372            attributes.push(attr);
373        }
374    }
375
376    Ok(Field::new(
377        name,
378        field_type,
379        modifier,
380        attributes,
381        Span::new(span.start(), span.end()),
382    ))
383}
384
385/// Parse a field type with optional modifier.
386fn parse_field_type(
387    pair: pest::iterators::Pair<'_, Rule>,
388) -> SchemaResult<(FieldType, TypeModifier)> {
389    let mut type_name = String::new();
390    let mut modifier = TypeModifier::Required;
391
392    for item in pair.into_inner() {
393        match item.as_rule() {
394            Rule::type_name => {
395                type_name = item.as_str().to_string();
396            }
397            Rule::optional_marker => {
398                modifier = if modifier == TypeModifier::List {
399                    TypeModifier::OptionalList
400                } else {
401                    TypeModifier::Optional
402                };
403            }
404            Rule::list_marker => {
405                modifier = if modifier == TypeModifier::Optional {
406                    TypeModifier::OptionalList
407                } else {
408                    TypeModifier::List
409                };
410            }
411            _ => {}
412        }
413    }
414
415    let field_type = if let Some(scalar) = ScalarType::from_str(&type_name) {
416        FieldType::Scalar(scalar)
417    } else {
418        // Assume it's a reference to a model, enum, or composite type.
419        // `Validator::resolve_field_types` (validator.rs) rewrites this to
420        // `FieldType::Enum`/`FieldType::Composite` for declared enums and
421        // composite types during validation; unknown names are rejected there.
422        FieldType::Model(SmolStr::new(&type_name))
423    };
424
425    Ok((field_type, modifier))
426}
427
428/// Parse an attribute.
429fn parse_attribute(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<Attribute> {
430    let span = pair.as_span();
431    let mut inner = pair.into_inner();
432
433    let name_pair = inner.next().unwrap();
434    let name = Ident::new(
435        name_pair.as_str(),
436        Span::new(name_pair.as_span().start(), name_pair.as_span().end()),
437    );
438
439    let mut args = vec![];
440    for item in inner {
441        if item.as_rule() == Rule::attribute_args {
442            args = parse_attribute_args(item)?;
443        }
444    }
445
446    Ok(Attribute::new(
447        name,
448        args,
449        Span::new(span.start(), span.end()),
450    ))
451}
452
453/// Parse attribute arguments.
454fn parse_attribute_args(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<Vec<AttributeArg>> {
455    let mut args = vec![];
456
457    for item in pair.into_inner() {
458        if item.as_rule() == Rule::attribute_arg {
459            let arg = parse_attribute_arg(item)?;
460            args.push(arg);
461        }
462    }
463
464    Ok(args)
465}
466
467/// Parse a single attribute argument.
468fn parse_attribute_arg(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<AttributeArg> {
469    let span = pair.as_span();
470    let mut inner = pair.into_inner();
471
472    let first = inner.next().unwrap();
473
474    // Check if this is a named argument (name: value) or positional
475    if let Some(second) = inner.next() {
476        // Named argument
477        let name = Ident::new(
478            first.as_str(),
479            Span::new(first.as_span().start(), first.as_span().end()),
480        );
481        let value = parse_attribute_value(second)?;
482        Ok(AttributeArg::named(
483            name,
484            value,
485            Span::new(span.start(), span.end()),
486        ))
487    } else {
488        // Positional argument
489        let value = parse_attribute_value(first)?;
490        Ok(AttributeArg::positional(
491            value,
492            Span::new(span.start(), span.end()),
493        ))
494    }
495}
496
497/// Parse an attribute value.
498fn parse_attribute_value(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<AttributeValue> {
499    match pair.as_rule() {
500        Rule::string_literal => {
501            let s = pair.as_str();
502            // Remove quotes and interpret `\"`/`\\` escapes.
503            Ok(AttributeValue::String(unquote_string_literal(s)))
504        }
505        Rule::number_literal => {
506            let s = pair.as_str();
507            if s.contains('.') {
508                Ok(AttributeValue::Float(s.parse().unwrap()))
509            } else {
510                Ok(AttributeValue::Int(s.parse().unwrap()))
511            }
512        }
513        Rule::boolean_literal => Ok(AttributeValue::Boolean(pair.as_str() == "true")),
514        Rule::identifier => Ok(AttributeValue::Ident(SmolStr::new(pair.as_str()))),
515        Rule::dotted_identifier => {
516            // Represent "rel.field" as a String so callers can split on '.'
517            Ok(AttributeValue::String(pair.as_str().to_string()))
518        }
519        Rule::function_call => {
520            let mut inner = pair.into_inner();
521            let name = SmolStr::new(inner.next().unwrap().as_str());
522            let mut args = vec![];
523            for item in inner {
524                args.push(parse_attribute_value(item)?);
525            }
526            Ok(AttributeValue::Function(name, args))
527        }
528        Rule::field_ref_list => {
529            let refs: Vec<SmolStr> = pair
530                .into_inner()
531                .map(|p| SmolStr::new(p.as_str()))
532                .collect();
533            Ok(AttributeValue::FieldRefList(refs))
534        }
535        Rule::array_literal => {
536            let values: Result<Vec<_>, _> = pair.into_inner().map(parse_attribute_value).collect();
537            Ok(AttributeValue::Array(values?))
538        }
539        Rule::attribute_value => {
540            // Unwrap nested attribute_value
541            parse_attribute_value(pair.into_inner().next().unwrap())
542        }
543        _ => {
544            // Fallback: treat as identifier
545            Ok(AttributeValue::Ident(SmolStr::new(pair.as_str())))
546        }
547    }
548}
549
550/// Parse a raw SQL definition.
551fn parse_raw_sql(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<RawSql> {
552    let mut inner = pair.into_inner();
553
554    let name = inner.next().unwrap().as_str();
555    let sql = inner.next().unwrap().as_str();
556
557    // Remove the surrounding double quotes from the name (the grammar token
558    // is the quoted string literal, consistent with other string parsing
559    // here) and interpret `\"`/`\\` escapes.
560    let name = unquote_string_literal(name.trim());
561
562    // Remove triple quotes
563    let sql_content = sql
564        .trim_start_matches("\"\"\"")
565        .trim_end_matches("\"\"\"")
566        .trim();
567
568    Ok(RawSql::new(name, sql_content))
569}
570
571/// Parse a server group definition.
572fn parse_server_group(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<ServerGroup> {
573    let span = pair.as_span();
574    let mut inner = pair.into_inner();
575
576    let name_pair = inner.next().unwrap();
577    let name = Ident::new(
578        name_pair.as_str(),
579        Span::new(name_pair.as_span().start(), name_pair.as_span().end()),
580    );
581
582    let mut server_group = ServerGroup::new(name, Span::new(span.start(), span.end()));
583
584    for item in inner {
585        match item.as_rule() {
586            Rule::server_group_item => {
587                // Unwrap the server_group_item to get the actual server_def or model_attribute
588                let inner_item = item.into_inner().next().unwrap();
589                match inner_item.as_rule() {
590                    Rule::server_def => {
591                        let server = parse_server(inner_item)?;
592                        server_group.add_server(server);
593                    }
594                    Rule::model_attribute => {
595                        let attr = parse_attribute(inner_item)?;
596                        server_group.add_attribute(attr);
597                    }
598                    _ => {}
599                }
600            }
601            Rule::server_def => {
602                let server = parse_server(item)?;
603                server_group.add_server(server);
604            }
605            Rule::model_attribute => {
606                let attr = parse_attribute(item)?;
607                server_group.add_attribute(attr);
608            }
609            _ => {}
610        }
611    }
612
613    Ok(server_group)
614}
615
616/// Parse a server definition within a server group.
617fn parse_server(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<Server> {
618    let span = pair.as_span();
619    let mut inner = pair.into_inner();
620
621    let name_pair = inner.next().unwrap();
622    let name = Ident::new(
623        name_pair.as_str(),
624        Span::new(name_pair.as_span().start(), name_pair.as_span().end()),
625    );
626
627    let mut server = Server::new(name, Span::new(span.start(), span.end()));
628
629    for item in inner {
630        if item.as_rule() == Rule::server_property {
631            let prop = parse_server_property(item)?;
632            server.add_property(prop);
633        }
634    }
635
636    Ok(server)
637}
638
639/// Parse a server property (key = value).
640fn parse_server_property(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<ServerProperty> {
641    let span = pair.as_span();
642    let mut inner = pair.into_inner();
643
644    let key_pair = inner.next().unwrap();
645    let key = key_pair.as_str();
646
647    let value_pair = inner.next().unwrap();
648    let value = parse_server_property_value(value_pair)?;
649
650    Ok(ServerProperty::new(
651        key,
652        value,
653        Span::new(span.start(), span.end()),
654    ))
655}
656
657/// Parse a generator definition.
658fn parse_generator(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<Generator> {
659    let span = pair.as_span();
660    let mut inner = pair.into_inner();
661
662    let name = inner.next().unwrap().as_str();
663    let mut generator = Generator::new(name, Span::new(span.start(), span.end()));
664
665    for prop in inner {
666        if prop.as_rule() == Rule::datasource_property {
667            let mut prop_inner = prop.into_inner();
668            let key = prop_inner.next().unwrap().as_str();
669            let value_pair = prop_inner.next().unwrap();
670
671            match key {
672                "provider" => {
673                    let s = extract_datasource_string(&value_pair);
674                    generator.provider = Some(SmolStr::new(s));
675                }
676                "output" => {
677                    let s = extract_datasource_string(&value_pair);
678                    generator.output = Some(SmolStr::new(s));
679                }
680                "generate" => {
681                    generator.generate = parse_generator_toggle(&value_pair);
682                }
683                _ => {
684                    let val = parse_generator_value(&value_pair);
685                    generator.properties.insert(SmolStr::new(key), val);
686                }
687            }
688        }
689    }
690
691    Ok(generator)
692}
693
694/// Unwrap one `datasource_value` layer from a pest pair, if present.
695///
696/// The grammar wraps every datasource/generator value in
697/// `datasource_value`; matching on the wrapped pair silently misses
698/// every concrete arm, so normalize before matching. Returns `None`
699/// only for an empty wrapper, which the grammar cannot produce.
700fn unwrap_datasource_value<'a>(
701    pair: &'a pest::iterators::Pair<'_, Rule>,
702) -> Option<pest::iterators::Pair<'a, Rule>> {
703    if pair.as_rule() == Rule::datasource_value {
704        pair.clone().into_inner().next()
705    } else {
706        Some(pair.clone())
707    }
708}
709
710/// Parse a generator toggle value (bool literal or env() call).
711fn parse_generator_toggle(pair: &pest::iterators::Pair<'_, Rule>) -> GeneratorToggle {
712    let unwrapped = unwrap_datasource_value(pair);
713    let pair = unwrapped.as_ref().unwrap_or(pair);
714    match pair.as_rule() {
715        Rule::env_function => {
716            let env_var = pair
717                .clone()
718                .into_inner()
719                .next()
720                .map(|p| {
721                    let s = p.as_str();
722                    SmolStr::new(unquote_string_literal(s))
723                })
724                .unwrap_or_default();
725            GeneratorToggle::Env(env_var)
726        }
727        _ => {
728            let s = pair.as_str().trim().trim_matches('"');
729            match s {
730                "true" => GeneratorToggle::Literal(true),
731                "false" => GeneratorToggle::Literal(false),
732                _ => GeneratorToggle::Literal(false),
733            }
734        }
735    }
736}
737
738/// Parse an arbitrary generator property value.
739fn parse_generator_value(pair: &pest::iterators::Pair<'_, Rule>) -> GeneratorValue {
740    let unwrapped = unwrap_datasource_value(pair);
741    let pair = unwrapped.as_ref().unwrap_or(pair);
742    match pair.as_rule() {
743        Rule::env_function => {
744            let env_var = pair
745                .clone()
746                .into_inner()
747                .next()
748                .map(|p| {
749                    let s = p.as_str();
750                    SmolStr::new(unquote_string_literal(s))
751                })
752                .unwrap_or_default();
753            GeneratorValue::Env(env_var)
754        }
755        Rule::string_literal => {
756            let s = pair.as_str();
757            GeneratorValue::String(SmolStr::new(unquote_string_literal(s)))
758        }
759        _ => {
760            let s = pair.as_str().trim().trim_matches('"');
761            match s {
762                "true" => GeneratorValue::Bool(true),
763                "false" => GeneratorValue::Bool(false),
764                _ => GeneratorValue::Ident(SmolStr::new(s)),
765            }
766        }
767    }
768}
769
770/// Parse a datasource definition.
771fn parse_datasource(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<Datasource> {
772    let span = pair.as_span();
773    let mut inner = pair.into_inner();
774
775    let name_pair = inner.next().unwrap();
776    let name = name_pair.as_str();
777
778    let mut datasource = Datasource::new(
779        name,
780        DatabaseProvider::PostgreSQL,
781        Span::new(span.start(), span.end()),
782    );
783
784    for prop in inner {
785        if prop.as_rule() == Rule::datasource_property {
786            let mut prop_inner = prop.into_inner();
787            let key = prop_inner.next().unwrap().as_str();
788            let value_pair = prop_inner.next().unwrap();
789
790            // The grammar wraps every value in `datasource_value`, so
791            // unwrap it before matching or the `url`/`extensions` arms
792            // below never fire and the values are silently dropped.
793            let value_pair = match unwrap_datasource_value(&value_pair) {
794                Some(inner) => inner,
795                None => {
796                    return Err(SchemaError::ConfigError {
797                        message: format!("datasource property `{key}` has an empty value"),
798                    });
799                }
800            };
801
802            match key {
803                "provider" => {
804                    let provider_str = extract_datasource_string(&value_pair);
805                    if let Some(provider) = DatabaseProvider::from_str(&provider_str) {
806                        datasource.provider = provider;
807                    }
808                }
809                "url" => {
810                    match value_pair.as_rule() {
811                        Rule::env_function => {
812                            // env("DATABASE_URL")
813                            let env_var = value_pair
814                                .into_inner()
815                                .next()
816                                .map(|p| {
817                                    let s = p.as_str();
818                                    unquote_string_literal(s)
819                                })
820                                .unwrap_or_default();
821                            datasource.url_env = Some(SmolStr::new(env_var));
822                        }
823                        Rule::string_literal => {
824                            let s = value_pair.as_str();
825                            let url = unquote_string_literal(s);
826                            datasource.url = Some(SmolStr::new(url));
827                        }
828                        _ => {
829                            return Err(SchemaError::ConfigError {
830                                message: format!(
831                                    "datasource property `url` must be a string literal or env(...) reference, found `{}`",
832                                    value_pair.as_str()
833                                ),
834                            });
835                        }
836                    }
837                }
838                "extensions" => {
839                    if value_pair.as_rule() == Rule::extension_array {
840                        for ext_item in value_pair.into_inner() {
841                            if ext_item.as_rule() == Rule::extension_item {
842                                let ext = parse_extension_item(
843                                    ext_item,
844                                    Span::new(span.start(), span.end()),
845                                )?;
846                                datasource.add_extension(ext);
847                            }
848                        }
849                    } else {
850                        return Err(SchemaError::ConfigError {
851                            message: format!(
852                                "datasource property `extensions` must be an array, found `{}`",
853                                value_pair.as_str()
854                            ),
855                        });
856                    }
857                }
858                _ => {
859                    // Store as additional property
860                    let value_str = extract_datasource_string(&value_pair);
861                    datasource.add_property(key, value_str);
862                }
863            }
864        }
865    }
866
867    Ok(datasource)
868}
869
870/// Parse an extension item from the extensions array.
871fn parse_extension_item(
872    pair: pest::iterators::Pair<'_, Rule>,
873    span: Span,
874) -> SchemaResult<PostgresExtension> {
875    let mut inner = pair.into_inner();
876    let name = inner.next().unwrap().as_str();
877    let mut ext = PostgresExtension::new(name, span);
878
879    // Check for extension args like (schema: "public", version: "0.5.0")
880    if let Some(args_pair) = inner.next()
881        && args_pair.as_rule() == Rule::extension_args
882    {
883        for arg in args_pair.into_inner() {
884            if arg.as_rule() == Rule::extension_arg {
885                let mut arg_inner = arg.into_inner();
886                let arg_key = arg_inner.next().unwrap().as_str();
887                let arg_value_pair = arg_inner.next().unwrap();
888                let arg_value = {
889                    let s = arg_value_pair.as_str();
890                    unquote_string_literal(s)
891                };
892
893                match arg_key {
894                    "schema" => {
895                        ext = ext.with_schema(arg_value);
896                    }
897                    "version" => {
898                        // The version is interpolated into `VERSION '...'`
899                        // SQL, so restrict its charset at the trust
900                        // boundary instead of emitting an injection
901                        // primitive downstream.
902                        if arg_value.is_empty()
903                            || !arg_value.chars().all(|c| {
904                                c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-'
905                            })
906                        {
907                            return Err(SchemaError::ConfigError {
908                                message: format!(
909                                    "extension version must match [A-Za-z0-9._-]+, found `{arg_value}`"
910                                ),
911                            });
912                        }
913                        ext = ext.with_version(arg_value);
914                    }
915                    _ => {}
916                }
917            }
918        }
919    }
920
921    Ok(ext)
922}
923
924/// Extract a string value from a datasource property value.
925fn extract_datasource_string(pair: &pest::iterators::Pair<'_, Rule>) -> String {
926    match pair.as_rule() {
927        Rule::string_literal => {
928            let s = pair.as_str();
929            unquote_string_literal(s)
930        }
931        Rule::identifier => pair.as_str().to_string(),
932        Rule::datasource_value => match unwrap_datasource_value(pair) {
933            Some(inner) => extract_datasource_string(&inner),
934            None => pair.as_str().to_string(),
935        },
936        _ => pair.as_str().to_string(),
937    }
938}
939
940/// Extract a string value from a pest pair, handling nesting.
941fn extract_string_from_arg(pair: pest::iterators::Pair<'_, Rule>) -> String {
942    match pair.as_rule() {
943        Rule::string_literal => {
944            let s = pair.as_str();
945            unquote_string_literal(s)
946        }
947        Rule::attribute_value => {
948            // Unwrap nested attribute_value
949            if let Some(inner) = pair.into_inner().next() {
950                extract_string_from_arg(inner)
951            } else {
952                String::new()
953            }
954        }
955        _ => pair.as_str().to_string(),
956    }
957}
958
959/// Parse a server property value.
960fn parse_server_property_value(
961    pair: pest::iterators::Pair<'_, Rule>,
962) -> SchemaResult<ServerPropertyValue> {
963    match pair.as_rule() {
964        Rule::string_literal => {
965            let s = pair.as_str();
966            // Remove quotes and interpret `\"`/`\\` escapes.
967            Ok(ServerPropertyValue::String(unquote_string_literal(s)))
968        }
969        Rule::number_literal => {
970            let s = pair.as_str();
971            Ok(ServerPropertyValue::Number(s.parse().unwrap_or(0.0)))
972        }
973        Rule::boolean_literal => Ok(ServerPropertyValue::Boolean(pair.as_str() == "true")),
974        Rule::identifier => Ok(ServerPropertyValue::Identifier(pair.as_str().to_string())),
975        Rule::function_call => {
976            // Handle env("VAR") and other function calls
977            let mut inner = pair.into_inner();
978            let func_name = inner.next().unwrap().as_str();
979            if func_name == "env"
980                && let Some(arg) = inner.next()
981            {
982                let var_name = extract_string_from_arg(arg);
983                return Ok(ServerPropertyValue::EnvVar(var_name));
984            }
985            // For other functions, store as identifier
986            Ok(ServerPropertyValue::Identifier(func_name.to_string()))
987        }
988        Rule::array_literal => {
989            let values: Result<Vec<_>, _> =
990                pair.into_inner().map(parse_server_property_value).collect();
991            Ok(ServerPropertyValue::Array(values?))
992        }
993        Rule::attribute_value => {
994            // Unwrap nested attribute_value
995            parse_server_property_value(pair.into_inner().next().unwrap())
996        }
997        _ => {
998            // Fallback: treat as identifier
999            Ok(ServerPropertyValue::Identifier(pair.as_str().to_string()))
1000        }
1001    }
1002}
1003
1004/// Parse a PostgreSQL Row-Level Security policy definition.
1005fn parse_policy(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<Policy> {
1006    let span = pair.as_span();
1007    let mut inner = pair.into_inner();
1008
1009    // First identifier is the policy name
1010    let name_pair = inner.next().unwrap();
1011    let name = Ident::new(
1012        name_pair.as_str(),
1013        Span::new(name_pair.as_span().start(), name_pair.as_span().end()),
1014    );
1015
1016    // Second identifier is the table name
1017    let table_pair = inner.next().unwrap();
1018    let table = Ident::new(
1019        table_pair.as_str(),
1020        Span::new(table_pair.as_span().start(), table_pair.as_span().end()),
1021    );
1022
1023    let mut policy = Policy::new(name, table, Span::new(span.start(), span.end()));
1024    // Reset commands to empty - will be set by 'for' clause if present
1025    policy.commands = vec![];
1026
1027    for item in inner {
1028        match item.as_rule() {
1029            Rule::policy_item => {
1030                let inner_item = item.into_inner().next().unwrap();
1031                parse_policy_item(&mut policy, inner_item)?;
1032            }
1033            Rule::policy_for
1034            | Rule::policy_to
1035            | Rule::policy_as
1036            | Rule::policy_using
1037            | Rule::policy_check => {
1038                parse_policy_item(&mut policy, item)?;
1039            }
1040            _ => {}
1041        }
1042    }
1043
1044    // Default to ALL if no commands specified
1045    if policy.commands.is_empty() {
1046        policy.commands.push(PolicyCommand::All);
1047    }
1048
1049    Ok(policy)
1050}
1051
1052/// Parse a single policy item (for, to, as, using, check, mssqlSchema, mssqlBlock).
1053fn parse_policy_item(
1054    policy: &mut Policy,
1055    pair: pest::iterators::Pair<'_, Rule>,
1056) -> SchemaResult<()> {
1057    match pair.as_rule() {
1058        Rule::policy_for => {
1059            let inner = pair.into_inner().next().unwrap();
1060            match inner.as_rule() {
1061                Rule::policy_command => {
1062                    if let Some(cmd) = PolicyCommand::from_str(inner.as_str()) {
1063                        policy.add_command(cmd);
1064                    }
1065                }
1066                Rule::policy_command_list => {
1067                    for cmd_pair in inner.into_inner() {
1068                        if cmd_pair.as_rule() == Rule::policy_command
1069                            && let Some(cmd) = PolicyCommand::from_str(cmd_pair.as_str())
1070                        {
1071                            policy.add_command(cmd);
1072                        }
1073                    }
1074                }
1075                _ => {}
1076            }
1077        }
1078        Rule::policy_to => {
1079            let inner = pair.into_inner().next().unwrap();
1080            match inner.as_rule() {
1081                Rule::identifier => {
1082                    policy.add_role(inner.as_str());
1083                }
1084                Rule::policy_role_list => {
1085                    for role_pair in inner.into_inner() {
1086                        if role_pair.as_rule() == Rule::identifier {
1087                            policy.add_role(role_pair.as_str());
1088                        }
1089                    }
1090                }
1091                _ => {}
1092            }
1093        }
1094        Rule::policy_as => {
1095            let inner = pair.into_inner().next().unwrap();
1096            if inner.as_rule() == Rule::policy_type
1097                && let Some(policy_type) = PolicyType::from_str(inner.as_str())
1098            {
1099                policy.policy_type = policy_type;
1100            }
1101        }
1102        Rule::policy_using => {
1103            let inner = pair.into_inner().next().unwrap();
1104            let expr = extract_policy_expression(&inner);
1105            policy.using_expr = Some(expr);
1106        }
1107        Rule::policy_check => {
1108            let inner = pair.into_inner().next().unwrap();
1109            let expr = extract_policy_expression(&inner);
1110            policy.check_expr = Some(expr);
1111        }
1112        Rule::policy_mssql_schema => {
1113            let inner = pair.into_inner().next().unwrap();
1114            if inner.as_rule() == Rule::string_literal {
1115                let s = inner.as_str();
1116                let schema = unquote_string_literal(s); // Remove quotes
1117                policy.mssql_schema = Some(SmolStr::new(schema));
1118            }
1119        }
1120        Rule::policy_mssql_block => {
1121            let inner = pair.into_inner().next().unwrap();
1122            match inner.as_rule() {
1123                Rule::mssql_block_op => {
1124                    if let Some(op) = MssqlBlockOperation::from_str(inner.as_str()) {
1125                        policy.add_mssql_block_operation(op);
1126                    }
1127                }
1128                Rule::mssql_block_op_list => {
1129                    for op_pair in inner.into_inner() {
1130                        if op_pair.as_rule() == Rule::mssql_block_op
1131                            && let Some(op) = MssqlBlockOperation::from_str(op_pair.as_str())
1132                        {
1133                            policy.add_mssql_block_operation(op);
1134                        }
1135                    }
1136                }
1137                _ => {}
1138            }
1139        }
1140        _ => {}
1141    }
1142    Ok(())
1143}
1144
1145/// Extract the expression from a string literal or multiline string.
1146fn extract_policy_expression(pair: &pest::iterators::Pair<'_, Rule>) -> String {
1147    let s = pair.as_str();
1148    match pair.as_rule() {
1149        Rule::multiline_string => {
1150            // Remove triple quotes
1151            s.trim_start_matches("\"\"\"")
1152                .trim_end_matches("\"\"\"")
1153                .trim()
1154                .to_string()
1155        }
1156        Rule::string_literal => {
1157            // Remove quotes and interpret `\"`/`\\` escapes.
1158            unquote_string_literal(s)
1159        }
1160        _ => s.to_string(),
1161    }
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166    use super::*;
1167
1168    // ==================== Basic Model Parsing ====================
1169
1170    #[test]
1171    fn test_parse_simple_model() {
1172        let schema = parse_schema(
1173            r#"
1174            model User {
1175                id    Int    @id @auto
1176                email String @unique
1177                name  String?
1178            }
1179        "#,
1180        )
1181        .unwrap();
1182
1183        assert_eq!(schema.models.len(), 1);
1184        let user = schema.get_model("User").unwrap();
1185        assert_eq!(user.fields.len(), 3);
1186        assert!(user.get_field("id").unwrap().is_id());
1187        assert!(user.get_field("email").unwrap().is_unique());
1188        assert!(user.get_field("name").unwrap().is_optional());
1189    }
1190
1191    #[test]
1192    fn test_parse_model_name() {
1193        let schema = parse_schema(
1194            r#"
1195            model BlogPost {
1196                id Int @id
1197            }
1198        "#,
1199        )
1200        .unwrap();
1201
1202        assert!(schema.get_model("BlogPost").is_some());
1203    }
1204
1205    #[test]
1206    fn test_parse_multiple_models() {
1207        let schema = parse_schema(
1208            r#"
1209            model User {
1210                id Int @id
1211            }
1212
1213            model Post {
1214                id Int @id
1215            }
1216
1217            model Comment {
1218                id Int @id
1219            }
1220        "#,
1221        )
1222        .unwrap();
1223
1224        assert_eq!(schema.models.len(), 3);
1225        assert!(schema.get_model("User").is_some());
1226        assert!(schema.get_model("Post").is_some());
1227        assert!(schema.get_model("Comment").is_some());
1228    }
1229
1230    // ==================== Field Type Parsing ====================
1231
1232    #[test]
1233    fn test_parse_all_scalar_types() {
1234        let schema = parse_schema(
1235            r#"
1236            model AllTypes {
1237                id       Int      @id
1238                big      BigInt
1239                float_f  Float
1240                decimal  Decimal
1241                str      String
1242                bool     Boolean
1243                datetime DateTime
1244                date     Date
1245                time     Time
1246                json     Json
1247                bytes    Bytes
1248                uuid     Uuid
1249                cuid     Cuid
1250                cuid2    Cuid2
1251                nanoid   NanoId
1252                ulid     Ulid
1253            }
1254        "#,
1255        )
1256        .unwrap();
1257
1258        let model = schema.get_model("AllTypes").unwrap();
1259        assert_eq!(model.fields.len(), 16);
1260
1261        assert!(matches!(
1262            model.get_field("id").unwrap().field_type,
1263            FieldType::Scalar(ScalarType::Int)
1264        ));
1265        assert!(matches!(
1266            model.get_field("big").unwrap().field_type,
1267            FieldType::Scalar(ScalarType::BigInt)
1268        ));
1269        assert!(matches!(
1270            model.get_field("str").unwrap().field_type,
1271            FieldType::Scalar(ScalarType::String)
1272        ));
1273        assert!(matches!(
1274            model.get_field("bool").unwrap().field_type,
1275            FieldType::Scalar(ScalarType::Boolean)
1276        ));
1277        assert!(matches!(
1278            model.get_field("datetime").unwrap().field_type,
1279            FieldType::Scalar(ScalarType::DateTime)
1280        ));
1281        assert!(matches!(
1282            model.get_field("uuid").unwrap().field_type,
1283            FieldType::Scalar(ScalarType::Uuid)
1284        ));
1285        assert!(matches!(
1286            model.get_field("cuid").unwrap().field_type,
1287            FieldType::Scalar(ScalarType::Cuid)
1288        ));
1289        assert!(matches!(
1290            model.get_field("cuid2").unwrap().field_type,
1291            FieldType::Scalar(ScalarType::Cuid2)
1292        ));
1293        assert!(matches!(
1294            model.get_field("nanoid").unwrap().field_type,
1295            FieldType::Scalar(ScalarType::NanoId)
1296        ));
1297        assert!(matches!(
1298            model.get_field("ulid").unwrap().field_type,
1299            FieldType::Scalar(ScalarType::Ulid)
1300        ));
1301    }
1302
1303    #[test]
1304    fn test_parse_optional_field() {
1305        let schema = parse_schema(
1306            r#"
1307            model User {
1308                id   Int     @id
1309                bio  String?
1310                age  Int?
1311            }
1312        "#,
1313        )
1314        .unwrap();
1315
1316        let user = schema.get_model("User").unwrap();
1317        assert!(!user.get_field("id").unwrap().is_optional());
1318        assert!(user.get_field("bio").unwrap().is_optional());
1319        assert!(user.get_field("age").unwrap().is_optional());
1320    }
1321
1322    #[test]
1323    fn test_parse_list_field() {
1324        let schema = parse_schema(
1325            r#"
1326            model User {
1327                id    Int      @id
1328                tags  String[]
1329                posts Post[]
1330            }
1331        "#,
1332        )
1333        .unwrap();
1334
1335        let user = schema.get_model("User").unwrap();
1336        assert!(user.get_field("tags").unwrap().is_list());
1337        assert!(user.get_field("posts").unwrap().is_list());
1338    }
1339
1340    #[test]
1341    fn test_parse_optional_list_field() {
1342        let schema = parse_schema(
1343            r#"
1344            model User {
1345                id       Int       @id
1346                metadata String[]?
1347            }
1348        "#,
1349        )
1350        .unwrap();
1351
1352        let user = schema.get_model("User").unwrap();
1353        let metadata = user.get_field("metadata").unwrap();
1354        assert!(metadata.is_list());
1355        assert!(metadata.is_optional());
1356    }
1357
1358    // ==================== Attribute Parsing ====================
1359
1360    #[test]
1361    fn test_parse_id_attribute() {
1362        let schema = parse_schema(
1363            r#"
1364            model User {
1365                id Int @id
1366            }
1367        "#,
1368        )
1369        .unwrap();
1370
1371        let user = schema.get_model("User").unwrap();
1372        assert!(user.get_field("id").unwrap().is_id());
1373    }
1374
1375    #[test]
1376    fn test_parse_unique_attribute() {
1377        let schema = parse_schema(
1378            r#"
1379            model User {
1380                id    Int    @id
1381                email String @unique
1382            }
1383        "#,
1384        )
1385        .unwrap();
1386
1387        let user = schema.get_model("User").unwrap();
1388        assert!(user.get_field("email").unwrap().is_unique());
1389    }
1390
1391    #[test]
1392    fn test_parse_default_int() {
1393        let schema = parse_schema(
1394            r#"
1395            model Counter {
1396                id    Int @id
1397                count Int @default(0)
1398            }
1399        "#,
1400        )
1401        .unwrap();
1402
1403        let counter = schema.get_model("Counter").unwrap();
1404        let count_field = counter.get_field("count").unwrap();
1405        let attrs = count_field.extract_attributes();
1406        assert!(attrs.default.is_some());
1407        assert_eq!(attrs.default.unwrap().as_int(), Some(0));
1408    }
1409
1410    #[test]
1411    fn test_parse_default_string() {
1412        let schema = parse_schema(
1413            r#"
1414            model User {
1415                id     Int    @id
1416                status String @default("active")
1417            }
1418        "#,
1419        )
1420        .unwrap();
1421
1422        let user = schema.get_model("User").unwrap();
1423        let status = user.get_field("status").unwrap();
1424        let attrs = status.extract_attributes();
1425        assert!(attrs.default.is_some());
1426        assert_eq!(attrs.default.unwrap().as_string(), Some("active"));
1427    }
1428
1429    #[test]
1430    fn test_parse_default_boolean() {
1431        let schema = parse_schema(
1432            r#"
1433            model Post {
1434                id        Int     @id
1435                published Boolean @default(false)
1436            }
1437        "#,
1438        )
1439        .unwrap();
1440
1441        let post = schema.get_model("Post").unwrap();
1442        let published = post.get_field("published").unwrap();
1443        let attrs = published.extract_attributes();
1444        assert!(attrs.default.is_some());
1445        assert_eq!(attrs.default.unwrap().as_bool(), Some(false));
1446    }
1447
1448    #[test]
1449    fn test_parse_default_function() {
1450        let schema = parse_schema(
1451            r#"
1452            model User {
1453                id        Int      @id
1454                createdAt DateTime @default(now())
1455            }
1456        "#,
1457        )
1458        .unwrap();
1459
1460        let user = schema.get_model("User").unwrap();
1461        let created_at = user.get_field("createdAt").unwrap();
1462        let attrs = created_at.extract_attributes();
1463        assert!(attrs.default.is_some());
1464        if let Some(AttributeValue::Function(name, _)) = attrs.default {
1465            assert_eq!(name.as_str(), "now");
1466        } else {
1467            panic!("Expected function default");
1468        }
1469    }
1470
1471    #[test]
1472    fn test_parse_updated_at_attribute() {
1473        let schema = parse_schema(
1474            r#"
1475            model User {
1476                id        Int      @id
1477                updatedAt DateTime @updated_at
1478            }
1479        "#,
1480        )
1481        .unwrap();
1482
1483        let user = schema.get_model("User").unwrap();
1484        let updated_at = user.get_field("updatedAt").unwrap();
1485        let attrs = updated_at.extract_attributes();
1486        assert!(attrs.is_updated_at);
1487    }
1488
1489    #[test]
1490    fn test_parse_map_attribute() {
1491        let schema = parse_schema(
1492            r#"
1493            model User {
1494                id    Int    @id
1495                email String @map("email_address")
1496            }
1497        "#,
1498        )
1499        .unwrap();
1500
1501        let user = schema.get_model("User").unwrap();
1502        let email = user.get_field("email").unwrap();
1503        let attrs = email.extract_attributes();
1504        assert_eq!(attrs.map, Some("email_address".to_string()));
1505    }
1506
1507    #[test]
1508    fn test_parse_map_attribute_with_escaped_quote() {
1509        // A `\"` escape inside a string literal must round-trip to a
1510        // literal `"` — MySQL enum values are arbitrary text and can
1511        // contain quotes, which `db pull` pins via `@map("...")`.
1512        let schema = parse_schema(
1513            r#"
1514            model Task {
1515                id     Int    @id
1516                status String @map("say \"hi\"")
1517            }
1518        "#,
1519        )
1520        .unwrap();
1521
1522        let task = schema.get_model("Task").unwrap();
1523        let status = task.get_field("status").unwrap();
1524        let attrs = status.extract_attributes();
1525        assert_eq!(attrs.map, Some("say \"hi\"".to_string()));
1526    }
1527
1528    #[test]
1529    fn test_parse_map_attribute_with_escaped_backslash() {
1530        let schema = parse_schema(
1531            r#"
1532            model Task {
1533                id     Int    @id
1534                status String @map("a\\b")
1535            }
1536        "#,
1537        )
1538        .unwrap();
1539
1540        let task = schema.get_model("Task").unwrap();
1541        let status = task.get_field("status").unwrap();
1542        let attrs = status.extract_attributes();
1543        assert_eq!(attrs.map, Some("a\\b".to_string()));
1544    }
1545
1546    #[test]
1547    fn test_parse_map_attribute_with_unrecognized_escape_stays_literal() {
1548        // Only `\"` and `\\` are escapes; any other `\x` keeps its
1549        // backslash so schemas written before escape support parse
1550        // byte-identically.
1551        let schema = parse_schema(
1552            r#"
1553            model Task {
1554                id     Int    @id
1555                status String @map("a\nb")
1556            }
1557        "#,
1558        )
1559        .unwrap();
1560
1561        let task = schema.get_model("Task").unwrap();
1562        let status = task.get_field("status").unwrap();
1563        let attrs = status.extract_attributes();
1564        assert_eq!(attrs.map, Some("a\\nb".to_string()));
1565    }
1566
1567    #[test]
1568    fn test_escape_unescape_prax_string_round_trips() {
1569        for raw in ["plain", "say \"hi\"", "a\\b", "\\\"both\\\"", "trailing\\"] {
1570            assert_eq!(unescape_prax_string(&escape_prax_string(raw)), raw);
1571        }
1572        // Unrecognized escapes keep their backslash (pre-escape files).
1573        assert_eq!(unescape_prax_string("a\\nb"), "a\\nb");
1574        assert_eq!(escape_prax_string("say \"hi\""), "say \\\"hi\\\"");
1575    }
1576
1577    #[test]
1578    fn test_parse_multiple_attributes() {
1579        let schema = parse_schema(
1580            r#"
1581            model User {
1582                id    Int    @id @auto
1583                email String @unique @index
1584            }
1585        "#,
1586        )
1587        .unwrap();
1588
1589        let user = schema.get_model("User").unwrap();
1590        let id = user.get_field("id").unwrap();
1591        let email = user.get_field("email").unwrap();
1592
1593        let id_attrs = id.extract_attributes();
1594        assert!(id_attrs.is_id);
1595        assert!(id_attrs.is_auto);
1596
1597        let email_attrs = email.extract_attributes();
1598        assert!(email_attrs.is_unique);
1599        assert!(email_attrs.is_indexed);
1600    }
1601
1602    // ==================== Model Attribute Parsing ====================
1603
1604    #[test]
1605    fn test_parse_model_map_attribute() {
1606        let schema = parse_schema(
1607            r#"
1608            model User {
1609                id Int @id
1610
1611                @@map("app_users")
1612            }
1613        "#,
1614        )
1615        .unwrap();
1616
1617        let user = schema.get_model("User").unwrap();
1618        assert_eq!(user.table_name(), "app_users");
1619    }
1620
1621    #[test]
1622    fn test_parse_model_index_attribute() {
1623        let schema = parse_schema(
1624            r#"
1625            model User {
1626                id    Int    @id
1627                email String
1628                name  String
1629
1630                @@index([email, name])
1631            }
1632        "#,
1633        )
1634        .unwrap();
1635
1636        let user = schema.get_model("User").unwrap();
1637        assert!(user.has_attribute("index"));
1638    }
1639
1640    #[test]
1641    fn test_parse_composite_primary_key() {
1642        let schema = parse_schema(
1643            r#"
1644            model PostTag {
1645                postId Int
1646                tagId  Int
1647
1648                @@id([postId, tagId])
1649            }
1650        "#,
1651        )
1652        .unwrap();
1653
1654        let post_tag = schema.get_model("PostTag").unwrap();
1655        assert!(post_tag.has_attribute("id"));
1656    }
1657
1658    // ==================== Enum Parsing ====================
1659
1660    #[test]
1661    fn test_parse_enum() {
1662        let schema = parse_schema(
1663            r#"
1664            enum Role {
1665                User
1666                Admin
1667                Moderator
1668            }
1669        "#,
1670        )
1671        .unwrap();
1672
1673        assert_eq!(schema.enums.len(), 1);
1674        let role = schema.get_enum("Role").unwrap();
1675        assert_eq!(role.variants.len(), 3);
1676    }
1677
1678    #[test]
1679    fn test_parse_enum_variant_names() {
1680        let schema = parse_schema(
1681            r#"
1682            enum Status {
1683                Pending
1684                Active
1685                Completed
1686                Cancelled
1687            }
1688        "#,
1689        )
1690        .unwrap();
1691
1692        let status = schema.get_enum("Status").unwrap();
1693        assert!(status.get_variant("Pending").is_some());
1694        assert!(status.get_variant("Active").is_some());
1695        assert!(status.get_variant("Completed").is_some());
1696        assert!(status.get_variant("Cancelled").is_some());
1697    }
1698
1699    #[test]
1700    fn test_parse_enum_with_map() {
1701        let schema = parse_schema(
1702            r#"
1703            enum Role {
1704                User  @map("USER")
1705                Admin @map("ADMINISTRATOR")
1706            }
1707        "#,
1708        )
1709        .unwrap();
1710
1711        let role = schema.get_enum("Role").unwrap();
1712        let user_variant = role.get_variant("User").unwrap();
1713        assert_eq!(user_variant.db_value(), "USER");
1714
1715        let admin_variant = role.get_variant("Admin").unwrap();
1716        assert_eq!(admin_variant.db_value(), "ADMINISTRATOR");
1717    }
1718
1719    // ==================== Relation Parsing ====================
1720
1721    #[test]
1722    fn test_parse_one_to_many_relation() {
1723        let schema = parse_schema(
1724            r#"
1725            model User {
1726                id    Int    @id
1727                posts Post[]
1728            }
1729
1730            model Post {
1731                id       Int  @id
1732                authorId Int
1733                author   User @relation(fields: [authorId], references: [id])
1734            }
1735        "#,
1736        )
1737        .unwrap();
1738
1739        let user = schema.get_model("User").unwrap();
1740        let post = schema.get_model("Post").unwrap();
1741
1742        assert!(user.get_field("posts").unwrap().is_list());
1743        assert!(post.get_field("author").unwrap().is_relation());
1744    }
1745
1746    #[test]
1747    fn test_parse_relation_with_actions() {
1748        let schema = parse_schema(
1749            r#"
1750            model Post {
1751                id       Int  @id
1752                authorId Int
1753                author   User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Restrict)
1754            }
1755
1756            model User {
1757                id    Int    @id
1758                posts Post[]
1759            }
1760        "#,
1761        )
1762        .unwrap();
1763
1764        let post = schema.get_model("Post").unwrap();
1765        let author = post.get_field("author").unwrap();
1766        let attrs = author.extract_attributes();
1767
1768        assert!(attrs.relation.is_some());
1769        let rel = attrs.relation.unwrap();
1770        assert_eq!(rel.on_delete, Some(ReferentialAction::Cascade));
1771        assert_eq!(rel.on_update, Some(ReferentialAction::Restrict));
1772    }
1773
1774    // ==================== Documentation Parsing ====================
1775
1776    #[test]
1777    fn test_parse_model_documentation() {
1778        let schema = parse_schema(
1779            r#"/// Represents a user in the system
1780model User {
1781    id Int @id
1782}"#,
1783        )
1784        .unwrap();
1785
1786        let user = schema.get_model("User").unwrap();
1787        // Documentation parsing is optional - the model should still parse
1788        // If documentation is present, it should contain "user"
1789        if let Some(doc) = &user.documentation {
1790            assert!(doc.text.contains("user"));
1791        }
1792    }
1793
1794    // ==================== Complete Schema Parsing ====================
1795
1796    #[test]
1797    fn test_parse_complete_schema() {
1798        let schema = parse_schema(
1799            r#"
1800            /// User model
1801            model User {
1802                id        Int      @id @auto
1803                email     String   @unique
1804                name      String?
1805                role      Role     @default(User)
1806                posts     Post[]
1807                profile   Profile?
1808                createdAt DateTime @default(now())
1809                updatedAt DateTime @updated_at
1810
1811                @@map("users")
1812                @@index([email])
1813            }
1814
1815            model Post {
1816                id        Int      @id @auto
1817                title     String
1818                content   String?
1819                published Boolean  @default(false)
1820                authorId  Int
1821                author    User     @relation(fields: [authorId], references: [id])
1822                tags      Tag[]
1823                createdAt DateTime @default(now())
1824
1825                @@index([authorId])
1826            }
1827
1828            model Profile {
1829                id     Int    @id @auto
1830                bio    String?
1831                userId Int    @unique
1832                user   User   @relation(fields: [userId], references: [id])
1833            }
1834
1835            model Tag {
1836                id    Int    @id @auto
1837                name  String @unique
1838                posts Post[]
1839            }
1840
1841            enum Role {
1842                User
1843                Admin
1844                Moderator
1845            }
1846        "#,
1847        )
1848        .unwrap();
1849
1850        // Verify models
1851        assert_eq!(schema.models.len(), 4);
1852        assert!(schema.get_model("User").is_some());
1853        assert!(schema.get_model("Post").is_some());
1854        assert!(schema.get_model("Profile").is_some());
1855        assert!(schema.get_model("Tag").is_some());
1856
1857        // Verify enums
1858        assert_eq!(schema.enums.len(), 1);
1859        assert!(schema.get_enum("Role").is_some());
1860
1861        // Verify User model details
1862        let user = schema.get_model("User").unwrap();
1863        assert_eq!(user.table_name(), "users");
1864        assert_eq!(user.fields.len(), 8);
1865        assert!(user.has_attribute("index"));
1866
1867        // Verify relations
1868        let post = schema.get_model("Post").unwrap();
1869        assert!(post.get_field("author").unwrap().is_relation());
1870    }
1871
1872    // ==================== Error Handling ====================
1873
1874    #[test]
1875    fn test_parse_invalid_syntax() {
1876        let result = parse_schema("model { broken }");
1877        assert!(result.is_err());
1878    }
1879
1880    #[test]
1881    fn test_parse_empty_schema() {
1882        let schema = parse_schema("").unwrap();
1883        assert!(schema.models.is_empty());
1884        assert!(schema.enums.is_empty());
1885    }
1886
1887    #[test]
1888    fn test_parse_whitespace_only() {
1889        let schema = parse_schema("   \n\t   \n   ").unwrap();
1890        assert!(schema.models.is_empty());
1891    }
1892
1893    #[test]
1894    fn test_parse_comments_only() {
1895        let schema = parse_schema(
1896            r#"
1897            // This is a comment
1898            // Another comment
1899        "#,
1900        )
1901        .unwrap();
1902        assert!(schema.models.is_empty());
1903    }
1904
1905    // ==================== Edge Cases ====================
1906
1907    #[test]
1908    fn test_parse_model_with_no_fields() {
1909        // Models with no fields should still parse (might be invalid semantically but syntactically ok)
1910        let result = parse_schema(
1911            r#"
1912            model Empty {
1913            }
1914        "#,
1915        );
1916        // This might error or succeed depending on grammar - just verify it doesn't panic
1917        let _ = result;
1918    }
1919
1920    #[test]
1921    fn test_parse_long_identifier() {
1922        let schema = parse_schema(
1923            r#"
1924            model VeryLongModelNameThatIsStillValid {
1925                someVeryLongFieldNameThatShouldWork Int @id
1926            }
1927        "#,
1928        )
1929        .unwrap();
1930
1931        assert!(
1932            schema
1933                .get_model("VeryLongModelNameThatIsStillValid")
1934                .is_some()
1935        );
1936    }
1937
1938    #[test]
1939    fn test_parse_underscore_identifiers() {
1940        let schema = parse_schema(
1941            r#"
1942            model user_account {
1943                user_id     Int @id
1944                created_at  DateTime
1945            }
1946        "#,
1947        )
1948        .unwrap();
1949
1950        let model = schema.get_model("user_account").unwrap();
1951        assert!(model.get_field("user_id").is_some());
1952        assert!(model.get_field("created_at").is_some());
1953    }
1954
1955    #[test]
1956    fn test_parse_negative_default() {
1957        let schema = parse_schema(
1958            r#"
1959            model Config {
1960                id       Int @id
1961                minValue Int @default(-100)
1962            }
1963        "#,
1964        )
1965        .unwrap();
1966
1967        let config = schema.get_model("Config").unwrap();
1968        let min_value = config.get_field("minValue").unwrap();
1969        let attrs = min_value.extract_attributes();
1970        assert!(attrs.default.is_some());
1971    }
1972
1973    #[test]
1974    fn test_parse_float_default() {
1975        let schema = parse_schema(
1976            r#"
1977            model Product {
1978                id    Int   @id
1979                price Float @default(9.99)
1980            }
1981        "#,
1982        )
1983        .unwrap();
1984
1985        let product = schema.get_model("Product").unwrap();
1986        let price = product.get_field("price").unwrap();
1987        let attrs = price.extract_attributes();
1988        assert!(attrs.default.is_some());
1989    }
1990
1991    // ==================== Server Group Parsing ====================
1992
1993    #[test]
1994    fn test_parse_simple_server_group() {
1995        let schema = parse_schema(
1996            r#"
1997            serverGroup MainCluster {
1998                server primary {
1999                    url = "postgres://localhost/db"
2000                    role = "primary"
2001                }
2002            }
2003        "#,
2004        )
2005        .unwrap();
2006
2007        assert_eq!(schema.server_groups.len(), 1);
2008        let cluster = schema.get_server_group("MainCluster").unwrap();
2009        assert_eq!(cluster.servers.len(), 1);
2010        assert!(cluster.servers.contains_key("primary"));
2011    }
2012
2013    #[test]
2014    fn test_parse_server_group_with_multiple_servers() {
2015        let schema = parse_schema(
2016            r#"
2017            serverGroup ReadReplicas {
2018                server primary {
2019                    url = "postgres://primary.db.com/app"
2020                    role = "primary"
2021                    weight = 1
2022                }
2023
2024                server replica1 {
2025                    url = "postgres://replica1.db.com/app"
2026                    role = "replica"
2027                    weight = 2
2028                }
2029
2030                server replica2 {
2031                    url = "postgres://replica2.db.com/app"
2032                    role = "replica"
2033                    weight = 2
2034                }
2035            }
2036        "#,
2037        )
2038        .unwrap();
2039
2040        let cluster = schema.get_server_group("ReadReplicas").unwrap();
2041        assert_eq!(cluster.servers.len(), 3);
2042
2043        let primary = cluster.servers.get("primary").unwrap();
2044        assert_eq!(primary.role(), Some(ServerRole::Primary));
2045        assert_eq!(primary.weight(), Some(1));
2046
2047        let replica1 = cluster.servers.get("replica1").unwrap();
2048        assert_eq!(replica1.role(), Some(ServerRole::Replica));
2049        assert_eq!(replica1.weight(), Some(2));
2050    }
2051
2052    #[test]
2053    fn test_parse_server_group_with_attributes() {
2054        let schema = parse_schema(
2055            r#"
2056            serverGroup ProductionCluster {
2057                @@strategy(ReadReplica)
2058                @@loadBalance(RoundRobin)
2059
2060                server main {
2061                    url = "postgres://main/db"
2062                    role = "primary"
2063                }
2064            }
2065        "#,
2066        )
2067        .unwrap();
2068
2069        let cluster = schema.get_server_group("ProductionCluster").unwrap();
2070        assert!(cluster.attributes.iter().any(|a| a.name.name == "strategy"));
2071        assert!(
2072            cluster
2073                .attributes
2074                .iter()
2075                .any(|a| a.name.name == "loadBalance")
2076        );
2077    }
2078
2079    #[test]
2080    fn test_parse_server_group_with_env_vars() {
2081        let schema = parse_schema(
2082            r#"
2083            serverGroup EnvCluster {
2084                server db1 {
2085                    url = env("PRIMARY_DB_URL")
2086                    role = "primary"
2087                }
2088            }
2089        "#,
2090        )
2091        .unwrap();
2092
2093        let cluster = schema.get_server_group("EnvCluster").unwrap();
2094        let server = cluster.servers.get("db1").unwrap();
2095
2096        // Check that the URL is stored as an env var reference
2097        if let Some(ServerPropertyValue::EnvVar(var)) = server.get_property("url") {
2098            assert_eq!(var, "PRIMARY_DB_URL");
2099        } else {
2100            panic!("Expected env var for url property");
2101        }
2102    }
2103
2104    #[test]
2105    fn test_parse_server_group_with_boolean_property() {
2106        let schema = parse_schema(
2107            r#"
2108            serverGroup TestCluster {
2109                server replica {
2110                    url = "postgres://replica/db"
2111                    role = "replica"
2112                    readOnly = true
2113                }
2114            }
2115        "#,
2116        )
2117        .unwrap();
2118
2119        let cluster = schema.get_server_group("TestCluster").unwrap();
2120        let server = cluster.servers.get("replica").unwrap();
2121        assert!(server.is_read_only());
2122    }
2123
2124    #[test]
2125    fn test_parse_server_group_with_numeric_properties() {
2126        let schema = parse_schema(
2127            r#"
2128            serverGroup NumericCluster {
2129                server db {
2130                    url = "postgres://localhost/db"
2131                    weight = 5
2132                    priority = 1
2133                    maxConnections = 100
2134                }
2135            }
2136        "#,
2137        )
2138        .unwrap();
2139
2140        let cluster = schema.get_server_group("NumericCluster").unwrap();
2141        let server = cluster.servers.get("db").unwrap();
2142
2143        assert_eq!(server.weight(), Some(5));
2144        assert_eq!(server.priority(), Some(1));
2145        assert_eq!(server.max_connections(), Some(100));
2146    }
2147
2148    #[test]
2149    fn test_parse_server_group_with_region() {
2150        let schema = parse_schema(
2151            r#"
2152            serverGroup GeoCluster {
2153                server usEast {
2154                    url = "postgres://us-east.db.com/app"
2155                    role = "replica"
2156                    region = "us-east-1"
2157                }
2158
2159                server usWest {
2160                    url = "postgres://us-west.db.com/app"
2161                    role = "replica"
2162                    region = "us-west-2"
2163                }
2164            }
2165        "#,
2166        )
2167        .unwrap();
2168
2169        let cluster = schema.get_server_group("GeoCluster").unwrap();
2170
2171        let us_east = cluster.servers.get("usEast").unwrap();
2172        assert_eq!(us_east.region(), Some("us-east-1"));
2173
2174        let us_west = cluster.servers.get("usWest").unwrap();
2175        assert_eq!(us_west.region(), Some("us-west-2"));
2176
2177        // Test region filtering
2178        let us_east_servers = cluster.servers_in_region("us-east-1");
2179        assert_eq!(us_east_servers.len(), 1);
2180    }
2181
2182    #[test]
2183    fn test_parse_multiple_server_groups() {
2184        let schema = parse_schema(
2185            r#"
2186            serverGroup Cluster1 {
2187                server db1 {
2188                    url = "postgres://db1/app"
2189                }
2190            }
2191
2192            serverGroup Cluster2 {
2193                server db2 {
2194                    url = "postgres://db2/app"
2195                }
2196            }
2197
2198            serverGroup Cluster3 {
2199                server db3 {
2200                    url = "postgres://db3/app"
2201                }
2202            }
2203        "#,
2204        )
2205        .unwrap();
2206
2207        assert_eq!(schema.server_groups.len(), 3);
2208        assert!(schema.get_server_group("Cluster1").is_some());
2209        assert!(schema.get_server_group("Cluster2").is_some());
2210        assert!(schema.get_server_group("Cluster3").is_some());
2211    }
2212
2213    #[test]
2214    fn test_parse_schema_with_models_and_server_groups() {
2215        let schema = parse_schema(
2216            r#"
2217            model User {
2218                id    Int    @id @auto
2219                email String @unique
2220            }
2221
2222            serverGroup Database {
2223                @@strategy(ReadReplica)
2224
2225                server primary {
2226                    url = env("DATABASE_URL")
2227                    role = "primary"
2228                }
2229            }
2230
2231            model Post {
2232                id       Int    @id @auto
2233                title    String
2234                authorId Int
2235            }
2236        "#,
2237        )
2238        .unwrap();
2239
2240        assert_eq!(schema.models.len(), 2);
2241        assert!(schema.get_model("User").is_some());
2242        assert!(schema.get_model("Post").is_some());
2243
2244        assert_eq!(schema.server_groups.len(), 1);
2245        assert!(schema.get_server_group("Database").is_some());
2246    }
2247
2248    #[test]
2249    fn test_parse_server_group_with_health_check() {
2250        let schema = parse_schema(
2251            r#"
2252            serverGroup HealthyCluster {
2253                server monitored {
2254                    url = "postgres://localhost/db"
2255                    healthCheck = "/health"
2256                }
2257            }
2258        "#,
2259        )
2260        .unwrap();
2261
2262        let cluster = schema.get_server_group("HealthyCluster").unwrap();
2263        let server = cluster.servers.get("monitored").unwrap();
2264        assert_eq!(server.health_check(), Some("/health"));
2265    }
2266
2267    #[test]
2268    fn test_server_group_failover_order() {
2269        let schema = parse_schema(
2270            r#"
2271            serverGroup FailoverCluster {
2272                server db3 {
2273                    url = "postgres://db3/app"
2274                    priority = 3
2275                }
2276
2277                server db1 {
2278                    url = "postgres://db1/app"
2279                    priority = 1
2280                }
2281
2282                server db2 {
2283                    url = "postgres://db2/app"
2284                    priority = 2
2285                }
2286            }
2287        "#,
2288        )
2289        .unwrap();
2290
2291        let cluster = schema.get_server_group("FailoverCluster").unwrap();
2292        let ordered = cluster.failover_order();
2293
2294        assert_eq!(ordered[0].name.name.as_str(), "db1");
2295        assert_eq!(ordered[1].name.name.as_str(), "db2");
2296        assert_eq!(ordered[2].name.name.as_str(), "db3");
2297    }
2298
2299    #[test]
2300    fn test_server_group_names() {
2301        let schema = parse_schema(
2302            r#"
2303            serverGroup Alpha {
2304                server s1 { url = "pg://a" }
2305            }
2306            serverGroup Beta {
2307                server s2 { url = "pg://b" }
2308            }
2309        "#,
2310        )
2311        .unwrap();
2312
2313        let names: Vec<_> = schema.server_group_names().collect();
2314        assert_eq!(names.len(), 2);
2315        assert!(names.contains(&"Alpha"));
2316        assert!(names.contains(&"Beta"));
2317    }
2318
2319    // ==================== Policy Parsing ====================
2320
2321    #[test]
2322    fn test_parse_simple_policy() {
2323        let schema = parse_schema(
2324            r#"
2325            policy UserReadOwn on User {
2326                for SELECT
2327                using "id = current_user_id()"
2328            }
2329        "#,
2330        )
2331        .unwrap();
2332
2333        assert_eq!(schema.policies.len(), 1);
2334        let policy = schema.get_policy("UserReadOwn").unwrap();
2335        assert_eq!(policy.name(), "UserReadOwn");
2336        assert_eq!(policy.table(), "User");
2337        assert!(policy.applies_to(PolicyCommand::Select));
2338        assert!(!policy.applies_to(PolicyCommand::Insert));
2339        assert_eq!(policy.using_expr.as_deref(), Some("id = current_user_id()"));
2340    }
2341
2342    #[test]
2343    fn test_parse_policy_with_multiple_commands() {
2344        let schema = parse_schema(
2345            r#"
2346            policy UserModify on User {
2347                for [SELECT, UPDATE, DELETE]
2348                using "id = auth.uid()"
2349            }
2350        "#,
2351        )
2352        .unwrap();
2353
2354        let policy = schema.get_policy("UserModify").unwrap();
2355        assert!(policy.applies_to(PolicyCommand::Select));
2356        assert!(policy.applies_to(PolicyCommand::Update));
2357        assert!(policy.applies_to(PolicyCommand::Delete));
2358        assert!(!policy.applies_to(PolicyCommand::Insert));
2359    }
2360
2361    #[test]
2362    fn test_parse_policy_with_all_command() {
2363        let schema = parse_schema(
2364            r#"
2365            policy UserAll on User {
2366                for ALL
2367                using "true"
2368            }
2369        "#,
2370        )
2371        .unwrap();
2372
2373        let policy = schema.get_policy("UserAll").unwrap();
2374        assert!(policy.applies_to(PolicyCommand::Select));
2375        assert!(policy.applies_to(PolicyCommand::Insert));
2376        assert!(policy.applies_to(PolicyCommand::Update));
2377        assert!(policy.applies_to(PolicyCommand::Delete));
2378    }
2379
2380    #[test]
2381    fn test_parse_policy_with_roles() {
2382        let schema = parse_schema(
2383            r#"
2384            policy AuthenticatedRead on Document {
2385                for SELECT
2386                to authenticated
2387                using "true"
2388            }
2389        "#,
2390        )
2391        .unwrap();
2392
2393        let policy = schema.get_policy("AuthenticatedRead").unwrap();
2394        let roles = policy.effective_roles();
2395        assert!(roles.contains(&"authenticated"));
2396    }
2397
2398    #[test]
2399    fn test_parse_policy_with_multiple_roles() {
2400        let schema = parse_schema(
2401            r#"
2402            policy AdminModerator on Post {
2403                for [UPDATE, DELETE]
2404                to [admin, moderator]
2405                using "true"
2406            }
2407        "#,
2408        )
2409        .unwrap();
2410
2411        let policy = schema.get_policy("AdminModerator").unwrap();
2412        let roles = policy.effective_roles();
2413        assert!(roles.contains(&"admin"));
2414        assert!(roles.contains(&"moderator"));
2415    }
2416
2417    #[test]
2418    fn test_parse_policy_restrictive() {
2419        let schema = parse_schema(
2420            r#"
2421            policy OrgRestriction on Document {
2422                as RESTRICTIVE
2423                for SELECT
2424                using "org_id = current_org_id()"
2425            }
2426        "#,
2427        )
2428        .unwrap();
2429
2430        let policy = schema.get_policy("OrgRestriction").unwrap();
2431        assert!(policy.is_restrictive());
2432        assert!(!policy.is_permissive());
2433    }
2434
2435    #[test]
2436    fn test_parse_policy_permissive_explicit() {
2437        let schema = parse_schema(
2438            r#"
2439            policy Permissive on User {
2440                as PERMISSIVE
2441                for SELECT
2442                using "true"
2443            }
2444        "#,
2445        )
2446        .unwrap();
2447
2448        let policy = schema.get_policy("Permissive").unwrap();
2449        assert!(policy.is_permissive());
2450    }
2451
2452    #[test]
2453    fn test_parse_policy_with_check() {
2454        let schema = parse_schema(
2455            r#"
2456            policy InsertOwn on Post {
2457                for INSERT
2458                to authenticated
2459                check "author_id = current_user_id()"
2460            }
2461        "#,
2462        )
2463        .unwrap();
2464
2465        let policy = schema.get_policy("InsertOwn").unwrap();
2466        assert!(policy.applies_to(PolicyCommand::Insert));
2467        assert_eq!(
2468            policy.check_expr.as_deref(),
2469            Some("author_id = current_user_id()")
2470        );
2471        assert!(policy.using_expr.is_none());
2472    }
2473
2474    #[test]
2475    fn test_parse_policy_with_both_expressions() {
2476        let schema = parse_schema(
2477            r#"
2478            policy UpdateOwn on Post {
2479                for UPDATE
2480                using "author_id = current_user_id()"
2481                check "author_id = current_user_id()"
2482            }
2483        "#,
2484        )
2485        .unwrap();
2486
2487        let policy = schema.get_policy("UpdateOwn").unwrap();
2488        assert!(policy.using_expr.is_some());
2489        assert!(policy.check_expr.is_some());
2490    }
2491
2492    #[test]
2493    fn test_parse_policy_multiline_expression() {
2494        let schema = parse_schema(
2495            r#"
2496            policy ComplexCheck on Document {
2497                for SELECT
2498                using """
2499                    (is_public = true)
2500                    OR (owner_id = current_user_id())
2501                    OR (id IN (SELECT document_id FROM shares WHERE user_id = current_user_id()))
2502                """
2503            }
2504        "#,
2505        )
2506        .unwrap();
2507
2508        let policy = schema.get_policy("ComplexCheck").unwrap();
2509        assert!(policy.using_expr.is_some());
2510        let expr = policy.using_expr.as_ref().unwrap();
2511        assert!(expr.contains("is_public = true"));
2512        assert!(expr.contains("owner_id = current_user_id()"));
2513        assert!(expr.contains("SELECT document_id FROM shares"));
2514    }
2515
2516    #[test]
2517    fn test_parse_multiple_policies() {
2518        let schema = parse_schema(
2519            r#"
2520            policy UserRead on User {
2521                for SELECT
2522                using "true"
2523            }
2524
2525            policy UserInsert on User {
2526                for INSERT
2527                check "id = current_user_id()"
2528            }
2529
2530            policy PostRead on Post {
2531                for SELECT
2532                using "published = true OR author_id = current_user_id()"
2533            }
2534        "#,
2535        )
2536        .unwrap();
2537
2538        assert_eq!(schema.policies.len(), 3);
2539        assert!(schema.get_policy("UserRead").is_some());
2540        assert!(schema.get_policy("UserInsert").is_some());
2541        assert!(schema.get_policy("PostRead").is_some());
2542    }
2543
2544    #[test]
2545    fn test_parse_policy_with_model() {
2546        let schema = parse_schema(
2547            r#"
2548            model User {
2549                id    Int    @id @auto
2550                email String @unique
2551            }
2552
2553            policy UserReadOwn on User {
2554                for SELECT
2555                to authenticated
2556                using "id = auth.uid()"
2557            }
2558        "#,
2559        )
2560        .unwrap();
2561
2562        assert_eq!(schema.models.len(), 1);
2563        assert_eq!(schema.policies.len(), 1);
2564
2565        let policies = schema.policies_for("User");
2566        assert_eq!(policies.len(), 1);
2567        assert_eq!(policies[0].name(), "UserReadOwn");
2568    }
2569
2570    #[test]
2571    fn test_parse_policies_for_multiple_models() {
2572        let schema = parse_schema(
2573            r#"
2574            policy UserPolicy1 on User {
2575                for SELECT
2576                using "true"
2577            }
2578
2579            policy UserPolicy2 on User {
2580                for INSERT
2581                check "true"
2582            }
2583
2584            policy PostPolicy on Post {
2585                for SELECT
2586                using "true"
2587            }
2588        "#,
2589        )
2590        .unwrap();
2591
2592        assert_eq!(schema.policies_for("User").len(), 2);
2593        assert_eq!(schema.policies_for("Post").len(), 1);
2594        assert!(schema.has_policies("User"));
2595        assert!(schema.has_policies("Post"));
2596        assert!(!schema.has_policies("Comment"));
2597    }
2598
2599    #[test]
2600    fn test_parse_policy_default_all_command() {
2601        let schema = parse_schema(
2602            r#"
2603            policy DefaultAll on User {
2604                using "id = current_user_id()"
2605            }
2606        "#,
2607        )
2608        .unwrap();
2609
2610        let policy = schema.get_policy("DefaultAll").unwrap();
2611        // When no 'for' clause, should default to ALL
2612        assert!(policy.applies_to(PolicyCommand::All));
2613    }
2614
2615    #[test]
2616    fn test_parse_policy_case_insensitive_keywords() {
2617        let schema = parse_schema(
2618            r#"
2619            policy CaseTest on User {
2620                for select
2621                as permissive
2622                using "true"
2623            }
2624        "#,
2625        )
2626        .unwrap();
2627
2628        let policy = schema.get_policy("CaseTest").unwrap();
2629        assert!(policy.applies_to(PolicyCommand::Select));
2630        assert!(policy.is_permissive());
2631    }
2632
2633    #[test]
2634    fn test_parse_policy_sql_generation() {
2635        let schema = parse_schema(
2636            r#"
2637            model User {
2638                id Int @id
2639
2640                @@map("users")
2641            }
2642
2643            policy ReadOwn on User {
2644                for SELECT
2645                to authenticated
2646                using "id = auth.uid()"
2647            }
2648        "#,
2649        )
2650        .unwrap();
2651
2652        let policy = schema.get_policy("ReadOwn").unwrap();
2653        let sql = policy.to_sql("users");
2654
2655        assert!(sql.contains("CREATE POLICY ReadOwn ON users"));
2656        assert!(sql.contains("FOR SELECT"));
2657        assert!(sql.contains("TO authenticated"));
2658        assert!(sql.contains("USING (id = auth.uid())"));
2659    }
2660
2661    #[test]
2662    fn test_parse_policy_restrictive_sql() {
2663        let schema = parse_schema(
2664            r#"
2665            policy OrgBoundary on Document {
2666                as RESTRICTIVE
2667                for ALL
2668                using "org_id = current_org_id()"
2669            }
2670        "#,
2671        )
2672        .unwrap();
2673
2674        let policy = schema.get_policy("OrgBoundary").unwrap();
2675        let sql = policy.to_sql("documents");
2676
2677        assert!(sql.contains("AS RESTRICTIVE"));
2678    }
2679
2680    #[test]
2681    fn test_parse_policy_with_documentation() {
2682        let schema = parse_schema(
2683            r#"
2684            /// Users can only read their own data
2685            policy UserIsolation on User {
2686                for SELECT
2687                using "id = current_user_id()"
2688            }
2689        "#,
2690        )
2691        .unwrap();
2692
2693        let policy = schema.get_policy("UserIsolation").unwrap();
2694        if let Some(doc) = &policy.documentation {
2695            assert!(doc.text.contains("their own data"));
2696        }
2697    }
2698
2699    #[test]
2700    fn test_parse_complex_rls_schema() {
2701        let schema = parse_schema(
2702            r#"
2703            model Organization {
2704                id   Int    @id @auto
2705                name String
2706            }
2707
2708            model User {
2709                id    Int    @id @auto
2710                orgId Int
2711                email String @unique
2712            }
2713
2714            model Document {
2715                id       Int     @id @auto
2716                title    String
2717                ownerId  Int
2718                orgId    Int
2719                isPublic Boolean @default(false)
2720            }
2721
2722            /// Organization-level isolation
2723            policy OrgIsolation on Document {
2724                as RESTRICTIVE
2725                for ALL
2726                using "org_id = current_setting('app.current_org')::int"
2727            }
2728
2729            /// Users can read public documents
2730            policy PublicRead on Document {
2731                for SELECT
2732                using "is_public = true"
2733            }
2734
2735            /// Users can read their own documents
2736            policy OwnerRead on Document {
2737                for SELECT
2738                to authenticated
2739                using "owner_id = auth.uid()"
2740            }
2741
2742            /// Users can only modify their own documents
2743            policy OwnerModify on Document {
2744                for [UPDATE, DELETE]
2745                to authenticated
2746                using "owner_id = auth.uid()"
2747                check "owner_id = auth.uid()"
2748            }
2749
2750            /// Users can create documents in their org
2751            policy OrgInsert on Document {
2752                for INSERT
2753                to authenticated
2754                check "org_id = current_setting('app.current_org')::int"
2755            }
2756        "#,
2757        )
2758        .unwrap();
2759
2760        assert_eq!(schema.models.len(), 3);
2761        assert_eq!(schema.policies.len(), 5);
2762
2763        // Verify org isolation is restrictive
2764        let org_iso = schema.get_policy("OrgIsolation").unwrap();
2765        assert!(org_iso.is_restrictive());
2766
2767        // Verify all Document policies
2768        let doc_policies = schema.policies_for("Document");
2769        assert_eq!(doc_policies.len(), 5);
2770    }
2771
2772    // ==================== MSSQL Policy Parsing ====================
2773
2774    #[test]
2775    fn test_parse_policy_with_mssql_schema() {
2776        let schema = parse_schema(
2777            r#"
2778            policy UserFilter on User {
2779                for SELECT
2780                using "UserId = @UserId"
2781                mssqlSchema "RLS"
2782            }
2783        "#,
2784        )
2785        .unwrap();
2786
2787        let policy = schema.get_policy("UserFilter").unwrap();
2788        assert_eq!(policy.mssql_schema(), "RLS");
2789    }
2790
2791    #[test]
2792    fn test_parse_policy_with_mssql_block_single() {
2793        let schema = parse_schema(
2794            r#"
2795            policy UserInsert on User {
2796                for INSERT
2797                check "UserId = @UserId"
2798                mssqlBlock AFTER_INSERT
2799            }
2800        "#,
2801        )
2802        .unwrap();
2803
2804        let policy = schema.get_policy("UserInsert").unwrap();
2805        assert_eq!(policy.mssql_block_operations.len(), 1);
2806        assert_eq!(
2807            policy.mssql_block_operations[0],
2808            MssqlBlockOperation::AfterInsert
2809        );
2810    }
2811
2812    #[test]
2813    fn test_parse_policy_with_mssql_block_list() {
2814        let schema = parse_schema(
2815            r#"
2816            policy UserModify on User {
2817                for [INSERT, UPDATE, DELETE]
2818                check "UserId = @UserId"
2819                mssqlBlock [AFTER_INSERT, AFTER_UPDATE, BEFORE_DELETE]
2820            }
2821        "#,
2822        )
2823        .unwrap();
2824
2825        let policy = schema.get_policy("UserModify").unwrap();
2826        assert_eq!(policy.mssql_block_operations.len(), 3);
2827        assert!(
2828            policy
2829                .mssql_block_operations
2830                .contains(&MssqlBlockOperation::AfterInsert)
2831        );
2832        assert!(
2833            policy
2834                .mssql_block_operations
2835                .contains(&MssqlBlockOperation::AfterUpdate)
2836        );
2837        assert!(
2838            policy
2839                .mssql_block_operations
2840                .contains(&MssqlBlockOperation::BeforeDelete)
2841        );
2842    }
2843
2844    #[test]
2845    fn test_parse_policy_full_mssql_config() {
2846        let schema = parse_schema(
2847            r#"
2848            policy TenantIsolation on Order {
2849                for ALL
2850                using "TenantId = @TenantId"
2851                check "TenantId = @TenantId"
2852                mssqlSchema "MultiTenant"
2853                mssqlBlock [AFTER_INSERT, BEFORE_UPDATE, AFTER_UPDATE, BEFORE_DELETE]
2854            }
2855        "#,
2856        )
2857        .unwrap();
2858
2859        let policy = schema.get_policy("TenantIsolation").unwrap();
2860
2861        // Verify standard options
2862        assert!(policy.applies_to(PolicyCommand::All));
2863        assert!(policy.using_expr.is_some());
2864        assert!(policy.check_expr.is_some());
2865
2866        // Verify MSSQL options
2867        assert_eq!(policy.mssql_schema(), "MultiTenant");
2868        assert_eq!(policy.mssql_block_operations.len(), 4);
2869
2870        // Test SQL generation
2871        let mssql = policy.to_mssql_sql("dbo.Orders", "TenantId");
2872        assert!(mssql.schema_sql.contains("MultiTenant"));
2873        assert!(mssql.function_sql.contains("fn_TenantIsolation_predicate"));
2874    }
2875
2876    #[test]
2877    fn test_parse_policy_mssql_block_case_variants() {
2878        // Test different case variants for block operations
2879        let schema = parse_schema(
2880            r#"
2881            policy Test1 on User {
2882                for INSERT
2883                check "true"
2884                mssqlBlock after_insert
2885            }
2886        "#,
2887        )
2888        .unwrap();
2889
2890        let policy = schema.get_policy("Test1").unwrap();
2891        assert_eq!(policy.mssql_block_operations.len(), 1);
2892        assert_eq!(
2893            policy.mssql_block_operations[0],
2894            MssqlBlockOperation::AfterInsert
2895        );
2896    }
2897
2898    #[test]
2899    fn test_parse_mixed_postgres_mssql_schema() {
2900        let schema = parse_schema(
2901            r#"
2902            model User {
2903                id    Int    @id @auto
2904                email String @unique
2905            }
2906
2907            // PostgreSQL-style policy (works on both, MSSQL uses defaults)
2908            policy UserReadOwn on User {
2909                for SELECT
2910                to authenticated
2911                using "id = current_user_id()"
2912            }
2913
2914            // MSSQL-optimized policy with explicit settings
2915            policy UserModifyOwn on User {
2916                for [INSERT, UPDATE, DELETE]
2917                to authenticated
2918                using "id = current_user_id()"
2919                check "id = current_user_id()"
2920                mssqlSchema "Security"
2921                mssqlBlock [AFTER_INSERT, BEFORE_UPDATE, AFTER_UPDATE, BEFORE_DELETE]
2922            }
2923        "#,
2924        )
2925        .unwrap();
2926
2927        assert_eq!(schema.policies.len(), 2);
2928
2929        // First policy uses defaults for MSSQL
2930        let read_policy = schema.get_policy("UserReadOwn").unwrap();
2931        assert_eq!(read_policy.mssql_schema(), "Security"); // default
2932        assert!(read_policy.mssql_block_operations.is_empty()); // will use auto-generated
2933
2934        // Second policy has explicit MSSQL config
2935        let modify_policy = schema.get_policy("UserModifyOwn").unwrap();
2936        assert_eq!(modify_policy.mssql_schema(), "Security");
2937        assert_eq!(modify_policy.mssql_block_operations.len(), 4);
2938
2939        // Both should generate valid PostgreSQL SQL
2940        let pg_sql = read_policy.to_postgres_sql("users");
2941        assert!(pg_sql.contains("CREATE POLICY UserReadOwn ON users"));
2942
2943        // Both should generate valid MSSQL SQL
2944        let mssql = modify_policy.to_mssql_sql("dbo.Users", "id");
2945        assert!(mssql.policy_sql.contains("Security.UserModifyOwn"));
2946    }
2947
2948    // ==================== Datasource Parsing ====================
2949
2950    #[test]
2951    fn test_parse_datasource_url_env() {
2952        let schema = parse_schema(
2953            r#"
2954            datasource db {
2955                provider = "postgresql"
2956                url = env("DATABASE_URL")
2957            }
2958        "#,
2959        )
2960        .unwrap();
2961
2962        let ds = schema.datasource.expect("datasource parsed");
2963        assert_eq!(ds.provider, crate::ast::DatabaseProvider::PostgreSQL);
2964        assert_eq!(ds.url_env.as_deref(), Some("DATABASE_URL"));
2965        assert!(ds.url.is_none());
2966    }
2967
2968    #[test]
2969    fn test_parse_datasource_url_literal() {
2970        let schema = parse_schema(
2971            r#"
2972            datasource db {
2973                provider = "mysql"
2974                url = "mysql://localhost/mydb"
2975            }
2976        "#,
2977        )
2978        .unwrap();
2979
2980        let ds = schema.datasource.expect("datasource parsed");
2981        assert_eq!(ds.url.as_deref(), Some("mysql://localhost/mydb"));
2982        assert!(ds.url_env.is_none());
2983    }
2984
2985    #[test]
2986    fn test_parse_datasource_extensions() {
2987        let schema = parse_schema(
2988            r#"
2989            datasource db {
2990                provider = "postgresql"
2991                url = env("DATABASE_URL")
2992                extensions = [vector(version: "1.2.0"), pg_trgm]
2993            }
2994        "#,
2995        )
2996        .unwrap();
2997
2998        let ds = schema.datasource.expect("datasource parsed");
2999        assert_eq!(ds.extensions.len(), 2);
3000        assert!(ds.has_extension("vector"));
3001        assert!(ds.has_extension("pg_trgm"));
3002        let vector = ds
3003            .extensions
3004            .iter()
3005            .find(|e| e.name() == "vector")
3006            .expect("vector extension parsed");
3007        assert_eq!(vector.version.as_deref(), Some("1.2.0"));
3008    }
3009
3010    #[test]
3011    fn test_parse_datasource_url_ident_rejected() {
3012        let err = parse_schema(
3013            r#"
3014            datasource db {
3015                provider = "postgresql"
3016                url = not_a_url
3017            }
3018        "#,
3019        )
3020        .expect_err("bare-identifier url must be rejected, not silently dropped");
3021        assert!(
3022            err.to_string().contains("url"),
3023            "error should mention `url`: {err}"
3024        );
3025    }
3026
3027    #[test]
3028    fn test_parse_datasource_extensions_rejected() {
3029        let err = parse_schema(
3030            r#"
3031            datasource db {
3032                provider = "postgresql"
3033                url = env("DATABASE_URL")
3034                extensions = "vector"
3035            }
3036        "#,
3037        )
3038        .expect_err("non-array extensions must be rejected, not silently dropped");
3039        assert!(
3040            err.to_string().contains("extensions"),
3041            "error should mention `extensions`: {err}"
3042        );
3043    }
3044
3045    #[test]
3046    fn test_parse_datasource_extension_version_empty_rejected() {
3047        let err = parse_schema(
3048            r#"
3049            datasource db {
3050                provider = "postgresql"
3051                url = env("DATABASE_URL")
3052                extensions = [vector(version: "")]
3053            }
3054        "#,
3055        )
3056        .expect_err("empty extension version must be rejected");
3057        assert!(
3058            err.to_string().contains("version"),
3059            "error should mention `version`: {err}"
3060        );
3061    }
3062
3063    #[test]
3064    fn test_parse_datasource_extension_version_plus_rejected() {
3065        // Policy pin: the version charset is deliberately strict
3066        // ([A-Za-z0-9._-]+). `+` is harmless inside single quotes but
3067        // no real-world Postgres extension version uses it, so it stays
3068        // rejected rather than widening the injection-relevant charset.
3069        let err = parse_schema(
3070            r#"
3071            datasource db {
3072                provider = "postgresql"
3073                url = env("DATABASE_URL")
3074                extensions = [vector(version: "1.0+beta")]
3075            }
3076        "#,
3077        )
3078        .expect_err("extension version containing `+` must be rejected");
3079        assert!(
3080            err.to_string().contains("version"),
3081            "error should mention `version`: {err}"
3082        );
3083    }
3084}