1mod 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
21pub fn escape_prax_string(raw: &str) -> String {
28 raw.replace('\\', "\\\\").replace('"', "\\\"")
29}
30
31pub 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
56fn unquote_string_literal(s: &str) -> String {
59 unescape_prax_string(&s[1..s.len() - 1])
60}
61
62pub 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 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
161pub 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
173fn 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 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
218fn 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 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
263fn 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
286fn 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
309fn 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 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
354fn 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
385fn 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 FieldType::Model(SmolStr::new(&type_name))
423 };
424
425 Ok((field_type, modifier))
426}
427
428fn 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
453fn 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
467fn 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 if let Some(second) = inner.next() {
476 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 let value = parse_attribute_value(first)?;
490 Ok(AttributeArg::positional(
491 value,
492 Span::new(span.start(), span.end()),
493 ))
494 }
495}
496
497fn 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 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 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 parse_attribute_value(pair.into_inner().next().unwrap())
542 }
543 _ => {
544 Ok(AttributeValue::Ident(SmolStr::new(pair.as_str())))
546 }
547 }
548}
549
550fn 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 let name = unquote_string_literal(name.trim());
561
562 let sql_content = sql
564 .trim_start_matches("\"\"\"")
565 .trim_end_matches("\"\"\"")
566 .trim();
567
568 Ok(RawSql::new(name, sql_content))
569}
570
571fn 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 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
616fn 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
639fn 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
657fn 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
694fn 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
710fn 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
738fn 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
770fn 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 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 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 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
870fn 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 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 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
924fn 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
940fn 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 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
959fn 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 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 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 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 parse_server_property_value(pair.into_inner().next().unwrap())
996 }
997 _ => {
998 Ok(ServerPropertyValue::Identifier(pair.as_str().to_string()))
1000 }
1001 }
1002}
1003
1004fn parse_policy(pair: pest::iterators::Pair<'_, Rule>) -> SchemaResult<Policy> {
1006 let span = pair.as_span();
1007 let mut inner = pair.into_inner();
1008
1009 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 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 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 if policy.commands.is_empty() {
1046 policy.commands.push(PolicyCommand::All);
1047 }
1048
1049 Ok(policy)
1050}
1051
1052fn 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); 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
1145fn 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 s.trim_start_matches("\"\"\"")
1152 .trim_end_matches("\"\"\"")
1153 .trim()
1154 .to_string()
1155 }
1156 Rule::string_literal => {
1157 unquote_string_literal(s)
1159 }
1160 _ => s.to_string(),
1161 }
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166 use super::*;
1167
1168 #[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 #[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 #[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 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 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 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 #[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 #[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 #[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 #[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 if let Some(doc) = &user.documentation {
1790 assert!(doc.text.contains("user"));
1791 }
1792 }
1793
1794 #[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 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 assert_eq!(schema.enums.len(), 1);
1859 assert!(schema.get_enum("Role").is_some());
1860
1861 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 let post = schema.get_model("Post").unwrap();
1869 assert!(post.get_field("author").unwrap().is_relation());
1870 }
1871
1872 #[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 #[test]
1908 fn test_parse_model_with_no_fields() {
1909 let result = parse_schema(
1911 r#"
1912 model Empty {
1913 }
1914 "#,
1915 );
1916 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 #[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 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 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 #[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 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 let org_iso = schema.get_policy("OrgIsolation").unwrap();
2765 assert!(org_iso.is_restrictive());
2766
2767 let doc_policies = schema.policies_for("Document");
2769 assert_eq!(doc_policies.len(), 5);
2770 }
2771
2772 #[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 assert!(policy.applies_to(PolicyCommand::All));
2863 assert!(policy.using_expr.is_some());
2864 assert!(policy.check_expr.is_some());
2865
2866 assert_eq!(policy.mssql_schema(), "MultiTenant");
2868 assert_eq!(policy.mssql_block_operations.len(), 4);
2869
2870 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 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 let read_policy = schema.get_policy("UserReadOwn").unwrap();
2931 assert_eq!(read_policy.mssql_schema(), "Security"); assert!(read_policy.mssql_block_operations.is_empty()); 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 let pg_sql = read_policy.to_postgres_sql("users");
2941 assert!(pg_sql.contains("CREATE POLICY UserReadOwn ON users"));
2942
2943 let mssql = modify_policy.to_mssql_sql("dbo.Users", "id");
2945 assert!(mssql.policy_sql.contains("Security.UserModifyOwn"));
2946 }
2947
2948 #[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 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}