Skip to main content

trilogy_parser/
parser.rs

1// Dependency-resolution view over the full Trilogy grammar. Walks the
2// `trilogy.pest` parse tree produced by `TrilogyParser` and extracts only the
3// three statement kinds that matter for dependency ordering: imports,
4// datasources, and persists. Everything else is ignored.
5//
6// Historically this module had its own permissive grammar (`preql.pest`) that
7// could parse partial / malformed files. The strict grammar refuses those, so
8// directory_resolver callers now surface a warning for files that can't parse
9// cleanly (the lark/pest pipelines would reject them at compile time anyway).
10
11use crate::trilogy_parser::{Rule, TrilogyParser};
12use pest::iterators::Pair;
13use pest::Parser;
14use std::path::{Path, PathBuf};
15use thiserror::Error;
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct ImportStatement {
19    pub raw_path: String,
20    pub parent_dirs: usize,
21    pub alias: Option<String>,
22    pub is_stdlib: bool,
23}
24
25impl ImportStatement {
26    pub fn resolve(&self, working_dir: &Path) -> Option<PathBuf> {
27        if self.is_stdlib {
28            return None;
29        }
30
31        let mut base = working_dir.to_path_buf();
32        for _ in 0..self.parent_dirs {
33            base = base.parent()?.to_path_buf();
34        }
35        for part in self.raw_path.split('.') {
36            base.push(part);
37        }
38        base.set_extension("preql");
39        Some(base)
40    }
41
42    pub fn effective_alias(&self) -> &str {
43        self.alias
44            .as_deref()
45            .unwrap_or_else(|| self.raw_path.split('.').last().unwrap_or(&self.raw_path))
46    }
47}
48
49/// How a datasource is backed. Only `Literal` yields a physical address that
50/// can be joined against externally-observed state; `Templated` addresses
51/// resolve at run time and are surfaced raw rather than silently dropped.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum AddressKind {
54    /// `address x.y` or a backtick-quoted literal — a physical warehouse table.
55    Literal,
56    /// `address f`...`` — an f-string; `address` holds the raw template.
57    Templated,
58    /// `query ...` — a view over other assets; no physical table.
59    Query,
60    /// `file ...` — a local file source; `address` holds the raw spec.
61    File,
62}
63
64impl AddressKind {
65    pub fn as_str(&self) -> &'static str {
66        match self {
67            AddressKind::Literal => "literal",
68            AddressKind::Templated => "templated",
69            AddressKind::Query => "query",
70            AddressKind::File => "file",
71        }
72    }
73}
74
75impl std::fmt::Display for AddressKind {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        write!(f, "{}", self.as_str())
78    }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash)]
82pub struct DatasourceDeclaration {
83    pub name: String,
84    /// Physical address for `Literal` (quoting stripped), raw template for
85    /// `Templated`, raw path spec for `File`, `None` for `Query`.
86    pub address: Option<String>,
87    pub address_kind: AddressKind,
88    /// `root datasource` — a source the script reads, not a managed asset it writes.
89    pub is_root: bool,
90    /// `partial datasource` — covers only a subset of its grain, so it cannot
91    /// answer a query on its own.
92    pub is_partial: bool,
93    /// Declares a `partition by` clause.
94    pub is_partitioned: bool,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Hash)]
98pub struct PersistStatement {
99    pub mode: PersistMode,
100    pub target_datasource: String,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104pub enum PersistMode {
105    Append,
106    Overwrite,
107    Persist,
108}
109
110impl std::fmt::Display for PersistMode {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        match self {
113            PersistMode::Append => write!(f, "append"),
114            PersistMode::Overwrite => write!(f, "overwrite"),
115            PersistMode::Persist => write!(f, "persist"),
116        }
117    }
118}
119
120#[derive(Debug, Clone, Default)]
121pub struct ParsedFile {
122    pub imports: Vec<ImportStatement>,
123    pub datasources: Vec<DatasourceDeclaration>,
124    pub persists: Vec<PersistStatement>,
125}
126
127#[derive(Error, Debug)]
128pub enum ParseError {
129    #[error("Failed to parse file: {0}")]
130    PestError(#[from] pest::error::Error<Rule>),
131
132    #[error("Invalid import statement structure")]
133    InvalidImportStructure,
134
135    #[error("Invalid datasource statement structure")]
136    InvalidDatasourceStructure,
137
138    #[error("Invalid persist statement structure")]
139    InvalidPersistStructure,
140}
141
142pub fn parse_file(content: &str) -> Result<ParsedFile, ParseError> {
143    let mut pairs = TrilogyParser::parse(Rule::start, content)?;
144    let start = pairs
145        .next()
146        .ok_or(ParseError::InvalidImportStructure)?;
147
148    let mut result = ParsedFile::default();
149    for top in start.into_inner() {
150        if top.as_rule() != Rule::block {
151            continue;
152        }
153        // block = { statement ~ _TERMINATOR }; `statement` is silent, so its
154        // inner rule (import_statement, datasource, persist_statement, ...)
155        // appears as a direct child of block.
156        for stmt in top.into_inner() {
157            match stmt.as_rule() {
158                // `from x.y import a, b` is the same file edge as `import x.y`;
159                // the concept list rides in its own `import_concepts` child, so
160                // the shared extractor sees only the path (and alias) tokens.
161                // `self import as X` re-imports the current file under a
162                // namespace and creates no cross-file edge, so it is ignored.
163                Rule::import_statement | Rule::selective_import_statement => {
164                    result.imports.push(extract_import(stmt)?);
165                }
166                Rule::datasource => {
167                    if let Some(declaration) = extract_datasource(stmt)? {
168                        result.datasources.push(declaration);
169                    }
170                }
171                Rule::persist_statement => {
172                    result.persists.push(extract_persist(stmt)?);
173                }
174                _ => {}
175            }
176        }
177    }
178
179    Ok(result)
180}
181
182pub fn parse_imports(content: &str) -> Result<Vec<ImportStatement>, ParseError> {
183    Ok(parse_file(content)?.imports)
184}
185
186// import_statement = { ^"import" ~ IMPORT_DOT* ~ dotted_identifier_tail ~ (^"as" ~ IDENTIFIER)? }
187// `dotted_identifier_tail` is silent, so children are the IMPORT_DOT tokens
188// followed by IDENTIFIER tokens for every path component and the optional alias.
189fn extract_import(pair: Pair<Rule>) -> Result<ImportStatement, ParseError> {
190    let full_text = pair.as_str();
191    let mut n_dots = 0usize;
192    let mut idents: Vec<String> = Vec::new();
193    for child in pair.into_inner() {
194        match child.as_rule() {
195            Rule::IMPORT_DOT => n_dots += 1,
196            Rule::IDENTIFIER => idents.push(child.as_str().to_string()),
197            _ => {}
198        }
199    }
200    if idents.is_empty() {
201        return Err(ParseError::InvalidImportStructure);
202    }
203
204    // Whether the final identifier is an alias. `as` is a reserved keyword, so
205    // a bare `as` token inside the statement text is unambiguous.
206    let has_alias = full_text
207        .split_ascii_whitespace()
208        .any(|tok| tok.eq_ignore_ascii_case("as"));
209    let alias = if has_alias && idents.len() >= 2 {
210        Some(idents.pop().unwrap())
211    } else {
212        None
213    };
214
215    let raw_path = idents.join(".");
216    let is_stdlib = raw_path == "std" || raw_path.starts_with("std.");
217    // Historical convention: leading dot prefix `..` means "one level up", so
218    // the first dot is part of the relative-import syntax and each extra dot
219    // adds one parent traversal.
220    let parent_dirs = n_dots.saturating_sub(1);
221
222    Ok(ImportStatement {
223        raw_path,
224        parent_dirs,
225        alias,
226        is_stdlib,
227    })
228}
229
230// datasource = { DATASOURCE_ROOT? ~ DATASOURCE_TEMPLATE? ~ (DATASOURCE_PARTIAL | SHORTHAND_MODIFIER)? ~ "datasource" ~ IDENTIFIER ~ "(" ~ ... }
231// The first direct IDENTIFIER child is always the datasource name; the
232// backing (address | query | file) and the partition clause are direct
233// children as well. A `template datasource` has no backing and is not an
234// asset: it yields None.
235fn extract_datasource(pair: Pair<Rule>) -> Result<Option<DatasourceDeclaration>, ParseError> {
236    let mut name: Option<String> = None;
237    let mut address: Option<String> = None;
238    let mut address_kind: Option<AddressKind> = None;
239    let mut is_root = false;
240    let mut is_template = false;
241    let mut is_partial = false;
242    let mut is_partitioned = false;
243
244    for child in pair.into_inner() {
245        match child.as_rule() {
246            Rule::DATASOURCE_ROOT => is_root = true,
247            Rule::DATASOURCE_TEMPLATE => is_template = true,
248            Rule::DATASOURCE_PARTIAL => is_partial = true,
249            Rule::IDENTIFIER if name.is_none() => name = Some(child.as_str().to_string()),
250            // address = { "address" ~ (F_QUOTED_ADDRESS | QUOTED_ADDRESS | ADDRESS) }
251            Rule::address => {
252                let tok = child
253                    .into_inner()
254                    .next()
255                    .ok_or(ParseError::InvalidDatasourceStructure)?;
256                match tok.as_rule() {
257                    Rule::F_QUOTED_ADDRESS => {
258                        // f`...` — keep the raw template body; it cannot be
259                        // resolved statically and must not be dropped.
260                        address_kind = Some(AddressKind::Templated);
261                        address = Some(
262                            tok.as_str()
263                                .trim_start_matches(['f', 'F'])
264                                .trim_matches('`')
265                                .to_string(),
266                        );
267                    }
268                    Rule::QUOTED_ADDRESS => {
269                        // `...` with an optional inner '...' layer.
270                        address_kind = Some(AddressKind::Literal);
271                        address = Some(
272                            tok.as_str().trim_matches('`').trim_matches('\'').to_string(),
273                        );
274                    }
275                    _ => {
276                        address_kind = Some(AddressKind::Literal);
277                        address = Some(tok.as_str().to_string());
278                    }
279                }
280            }
281            Rule::query => address_kind = Some(AddressKind::Query),
282            Rule::file => {
283                address_kind = Some(AddressKind::File);
284                // Raw spec after the (always 4-byte) `file` keyword.
285                address = Some(child.as_str()[4..].trim().to_string());
286            }
287            Rule::datasource_partition_clause => is_partitioned = true,
288            _ => {}
289        }
290    }
291
292    if is_template {
293        return Ok(None);
294    }
295    match (name, address_kind) {
296        (Some(name), Some(address_kind)) => Ok(Some(DatasourceDeclaration {
297            name,
298            address,
299            address_kind,
300            is_root,
301            is_partial,
302            is_partitioned,
303        })),
304        _ => Err(ParseError::InvalidDatasourceStructure),
305    }
306}
307
308fn extract_persist(pair: Pair<Rule>) -> Result<PersistStatement, ParseError> {
309    // persist_statement = { full_persist | auto_persist }
310    let inner = pair
311        .into_inner()
312        .next()
313        .ok_or(ParseError::InvalidPersistStructure)?;
314    match inner.as_rule() {
315        Rule::auto_persist => extract_auto_persist(inner),
316        Rule::full_persist => extract_full_persist(inner),
317        _ => Err(ParseError::InvalidPersistStructure),
318    }
319}
320
321// auto_persist = { PERSIST_MODE ~ IDENTIFIER ~ where? }
322fn extract_auto_persist(pair: Pair<Rule>) -> Result<PersistStatement, ParseError> {
323    let mut mode: Option<PersistMode> = None;
324    let mut target: Option<String> = None;
325    for child in pair.into_inner() {
326        match child.as_rule() {
327            Rule::PERSIST_MODE => mode = Some(parse_persist_mode(child.as_str())),
328            Rule::IDENTIFIER if target.is_none() => {
329                target = Some(child.as_str().to_string());
330            }
331            _ => {}
332        }
333    }
334    match (mode, target) {
335        (Some(mode), Some(target_datasource)) => Ok(PersistStatement {
336            mode,
337            target_datasource,
338        }),
339        _ => Err(ParseError::InvalidPersistStructure),
340    }
341}
342
343// full_persist = { PERSIST_MODE ~ (!"into" ~ IDENTIFIER)? ~ "into" ~ IDENTIFIER ~ persist_partition_clause? ~ "from" ~ select_statement }
344// Literals (`into`, `from`) are not emitted as children, so we see PERSIST_MODE,
345// optionally a source IDENTIFIER, then the target IDENTIFIER, then the select
346// subtree. Taking the LAST direct IDENTIFIER yields the post-`into` target.
347fn extract_full_persist(pair: Pair<Rule>) -> Result<PersistStatement, ParseError> {
348    let mut mode: Option<PersistMode> = None;
349    let mut last_ident: Option<String> = None;
350    for child in pair.into_inner() {
351        match child.as_rule() {
352            Rule::PERSIST_MODE => mode = Some(parse_persist_mode(child.as_str())),
353            Rule::IDENTIFIER => last_ident = Some(child.as_str().to_string()),
354            _ => {}
355        }
356    }
357    match (mode, last_ident) {
358        (Some(mode), Some(target_datasource)) => Ok(PersistStatement {
359            mode,
360            target_datasource,
361        }),
362        _ => Err(ParseError::InvalidPersistStructure),
363    }
364}
365
366fn parse_persist_mode(s: &str) -> PersistMode {
367    match s.to_ascii_lowercase().as_str() {
368        "append" => PersistMode::Append,
369        "overwrite" => PersistMode::Overwrite,
370        _ => PersistMode::Persist,
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_simple_import() {
380        let parsed = parse_file("import models.customer;").unwrap();
381        assert_eq!(parsed.imports.len(), 1);
382        assert_eq!(parsed.imports[0].raw_path, "models.customer");
383        assert_eq!(parsed.imports[0].parent_dirs, 0);
384        assert!(parsed.imports[0].alias.is_none());
385    }
386
387    #[test]
388    fn test_import_with_alias() {
389        let parsed = parse_file("import models.customer as cust;").unwrap();
390        assert_eq!(parsed.imports.len(), 1);
391        assert_eq!(parsed.imports[0].raw_path, "models.customer");
392        assert_eq!(parsed.imports[0].alias, Some("cust".to_string()));
393    }
394
395    #[test]
396    fn test_relative_import() {
397        let parsed = parse_file("import ..models.customer;").unwrap();
398        assert_eq!(parsed.imports.len(), 1);
399        assert_eq!(parsed.imports[0].raw_path, "models.customer");
400        assert_eq!(parsed.imports[0].parent_dirs, 1);
401    }
402
403    #[test]
404    fn test_sibling_relative_import() {
405        let parsed = parse_file("import .customer;").unwrap();
406        assert_eq!(parsed.imports[0].raw_path, "customer");
407        assert_eq!(parsed.imports[0].parent_dirs, 0);
408    }
409
410    #[test]
411    fn test_stdlib_import() {
412        let parsed = parse_file("import std.aggregates;").unwrap();
413        assert!(parsed.imports[0].is_stdlib);
414    }
415
416    #[test]
417    fn test_datasource_simple() {
418        let content = r#"
419            key order_id int;
420            datasource orders (
421                order_id: order_id,
422                amount: amount
423            )
424            grain (order_id)
425            address my_database.orders;
426        "#;
427        let parsed = parse_file(content).unwrap();
428        assert_eq!(parsed.datasources.len(), 1);
429        let ds = &parsed.datasources[0];
430        assert_eq!(ds.name, "orders");
431        assert_eq!(ds.address.as_deref(), Some("my_database.orders"));
432        assert_eq!(ds.address_kind, AddressKind::Literal);
433        assert!(!ds.is_root);
434        assert!(!ds.is_partitioned);
435    }
436
437    #[test]
438    fn test_datasource_with_quoted_address() {
439        let content = r#"
440            key customer_id int;
441            datasource customers (
442                id: customer_id,
443                name: customer_name
444            )
445            grain (customer_id)
446            address `my_db.customers`;
447        "#;
448        let parsed = parse_file(content).unwrap();
449        assert_eq!(parsed.datasources.len(), 1);
450        let ds = &parsed.datasources[0];
451        assert_eq!(ds.name, "customers");
452        assert_eq!(ds.address.as_deref(), Some("my_db.customers"));
453        assert_eq!(ds.address_kind, AddressKind::Literal);
454    }
455
456    #[test]
457    fn test_root_partitioned_datasource() {
458        let content = r#"
459            key event_id int;
460            root datasource events (
461                event_id: event_id
462            )
463            grain (event_id)
464            address analytics.events
465            partition by event_id;
466        "#;
467        let parsed = parse_file(content).unwrap();
468        let ds = &parsed.datasources[0];
469        assert!(ds.is_root);
470        assert!(ds.is_partitioned);
471        assert_eq!(ds.address.as_deref(), Some("analytics.events"));
472    }
473
474    #[test]
475    fn test_templated_address_datasource() {
476        let content = r#"
477            key order_id int;
478            datasource orders (
479                order_id: order_id
480            )
481            grain (order_id)
482            address f`{{env}}.orders`;
483        "#;
484        let parsed = parse_file(content).unwrap();
485        let ds = &parsed.datasources[0];
486        assert_eq!(ds.address_kind, AddressKind::Templated);
487        // Raw template body is kept — it cannot be resolved statically.
488        assert_eq!(ds.address.as_deref(), Some("{{env}}.orders"));
489    }
490
491    #[test]
492    fn test_query_datasource_has_no_address() {
493        let content = r#"
494            key order_id int;
495            datasource order_view (
496                order_id: order_id
497            )
498            grain (order_id)
499            query '''select 1 as order_id''';
500        "#;
501        let parsed = parse_file(content).unwrap();
502        let ds = &parsed.datasources[0];
503        assert_eq!(ds.address_kind, AddressKind::Query);
504        assert!(ds.address.is_none());
505    }
506
507    #[test]
508    fn test_file_datasource() {
509        let content = r#"
510            key launch_id int;
511            datasource launches (
512                launch_id: launch_id
513            )
514            grain (launch_id)
515            file `gcs://bucket/launch_report/launch.parquet`;
516        "#;
517        let parsed = parse_file(content).unwrap();
518        let ds = &parsed.datasources[0];
519        assert_eq!(ds.address_kind, AddressKind::File);
520        // Raw spec: file paths may themselves contain `:` (`gcs://`), so the
521        // backticked form is kept verbatim rather than unquoted ambiguously.
522        assert_eq!(
523            ds.address.as_deref(),
524            Some("`gcs://bucket/launch_report/launch.parquet`")
525        );
526    }
527
528    #[test]
529    fn test_file_datasource_read_write_pair() {
530        let content = r#"
531            key launch_id int;
532            datasource launches (
533                launch_id: launch_id
534            )
535            grain (launch_id)
536            file `https://host/launch.parquet`:`gcs://bucket/launch.parquet`;
537        "#;
538        let parsed = parse_file(content).unwrap();
539        let ds = &parsed.datasources[0];
540        assert_eq!(ds.address_kind, AddressKind::File);
541        assert_eq!(
542            ds.address.as_deref(),
543            Some("`https://host/launch.parquet`:`gcs://bucket/launch.parquet`")
544        );
545    }
546
547    #[test]
548    fn test_root_partial_file_datasource() {
549        let content = r#"
550            key a_id int;
551            root partial datasource a_raw (
552                a_id: a_id
553            )
554            grain (a_id)
555            file `./a_raw_source.py`;
556        "#;
557        let parsed = parse_file(content).unwrap();
558        let ds = &parsed.datasources[0];
559        assert_eq!(ds.name, "a_raw");
560        assert!(ds.is_root);
561        assert!(ds.is_partial);
562        assert!(!ds.is_partitioned);
563        assert_eq!(ds.address_kind, AddressKind::File);
564    }
565
566    #[test]
567    fn test_partial_datasource_name_not_shadowed_by_modifier() {
568        let content = r#"
569            key customer_id int;
570            partial datasource customer_revenue (
571                customer_id: customer_id
572            )
573            grain (customer_id)
574            address db.customer_revenue;
575        "#;
576        let parsed = parse_file(content).unwrap();
577        let ds = &parsed.datasources[0];
578        assert_eq!(ds.name, "customer_revenue");
579        assert!(ds.is_partial);
580        assert!(!ds.is_root);
581    }
582
583    #[test]
584    fn test_multiple_datasources_keep_independent_flags() {
585        let content = r#"
586            key order_id int;
587            root datasource raw_orders (
588                order_id: order_id
589            )
590            grain (order_id)
591            address raw.orders;
592
593            datasource orders (
594                order_id: order_id
595            )
596            grain (order_id)
597            address db.orders;
598        "#;
599        let parsed = parse_file(content).unwrap();
600        assert_eq!(parsed.datasources.len(), 2);
601        assert!(parsed.datasources[0].is_root);
602        assert_eq!(parsed.datasources[0].address.as_deref(), Some("raw.orders"));
603        assert!(!parsed.datasources[1].is_root);
604        assert_eq!(parsed.datasources[1].address.as_deref(), Some("db.orders"));
605    }
606
607    #[test]
608    fn test_quoted_address_strips_inner_quote_layer() {
609        let content = r#"
610            key order_id int;
611            datasource orders (
612                order_id: order_id
613            )
614            grain (order_id)
615            address `'my project.my dataset.orders'`;
616        "#;
617        let parsed = parse_file(content).unwrap();
618        let ds = &parsed.datasources[0];
619        assert_eq!(ds.address_kind, AddressKind::Literal);
620        assert_eq!(
621            ds.address.as_deref(),
622            Some("my project.my dataset.orders")
623        );
624    }
625
626    #[test]
627    fn test_address_kind_strings_are_stable() {
628        // These are the serialized `address_kind` values in CLI JSON output.
629        assert_eq!(AddressKind::Literal.to_string(), "literal");
630        assert_eq!(AddressKind::Templated.to_string(), "templated");
631        assert_eq!(AddressKind::Query.to_string(), "query");
632        assert_eq!(AddressKind::File.to_string(), "file");
633    }
634
635    #[test]
636    fn test_selective_import_creates_edge() {
637        let parsed = parse_file("from models.customer import customer_id;").unwrap();
638        assert_eq!(parsed.imports.len(), 1);
639        assert_eq!(parsed.imports[0].raw_path, "models.customer");
640        assert!(parsed.imports[0].alias.is_none());
641    }
642
643    #[test]
644    fn test_selective_import_with_alias() {
645        let parsed = parse_file("from ..models.customer as cust import customer_id, name;").unwrap();
646        assert_eq!(parsed.imports.len(), 1);
647        assert_eq!(parsed.imports[0].raw_path, "models.customer");
648        assert_eq!(parsed.imports[0].alias, Some("cust".to_string()));
649        assert_eq!(parsed.imports[0].parent_dirs, 1);
650    }
651
652    #[test]
653    fn test_self_import_is_not_an_edge() {
654        let parsed = parse_file("self import as me;").unwrap();
655        assert!(parsed.imports.is_empty());
656    }
657
658    #[test]
659    fn test_auto_persist() {
660        let parsed = parse_file("persist orders;").unwrap();
661        assert_eq!(parsed.persists.len(), 1);
662        assert_eq!(parsed.persists[0].target_datasource, "orders");
663        assert_eq!(parsed.persists[0].mode, PersistMode::Persist);
664    }
665
666    #[test]
667    fn test_append_auto_persist() {
668        let parsed = parse_file("append orders;").unwrap();
669        assert_eq!(parsed.persists.len(), 1);
670        assert_eq!(parsed.persists[0].target_datasource, "orders");
671        assert_eq!(parsed.persists[0].mode, PersistMode::Append);
672    }
673
674    #[test]
675    fn test_full_persist() {
676        let content = r#"
677            key order_id int;
678            overwrite into target_orders from select order_id;
679        "#;
680        let parsed = parse_file(content).unwrap();
681        assert_eq!(parsed.persists.len(), 1);
682        assert_eq!(parsed.persists[0].target_datasource, "target_orders");
683        assert_eq!(parsed.persists[0].mode, PersistMode::Overwrite);
684    }
685
686    #[test]
687    fn test_multiple_imports() {
688        let content = r#"
689            import models.customer;
690            import models.orders as ord;
691            // comment
692            import ..shared.utils;
693        "#;
694        let parsed = parse_file(content).unwrap();
695        assert_eq!(parsed.imports.len(), 3);
696        assert_eq!(parsed.imports[1].alias, Some("ord".to_string()));
697        assert_eq!(parsed.imports[2].parent_dirs, 1);
698    }
699
700    #[test]
701    fn test_mixed_file() {
702        let content = r#"
703            import models.customer;
704
705            key order_id int;
706            datasource local_orders (
707                order_id: order_id
708            )
709            grain (order_id)
710            address local.orders;
711
712            persist local_orders;
713        "#;
714        let parsed = parse_file(content).unwrap();
715        assert_eq!(parsed.imports.len(), 1);
716        assert_eq!(parsed.datasources.len(), 1);
717        assert_eq!(parsed.persists.len(), 1);
718        assert_eq!(parsed.datasources[0].name, "local_orders");
719        assert_eq!(parsed.persists[0].target_datasource, "local_orders");
720    }
721}