lutra/
compile.rs

1use std::collections::HashMap;
2use std::str::FromStr;
3
4use anyhow::Result;
5use prqlc::ir::decl::RootModule;
6use prqlc::ir::pl::{Ident, Literal};
7use prqlc::sql::Dialect;
8use prqlc::{semantic, Error, ErrorMessages, Errors, Options, SourceTree, Target, WithErrorInfo};
9
10use crate::project::{DatabaseModule, ProjectCompiled, ProjectDiscovered, SqliteConnectionParams};
11
12#[cfg_attr(feature = "clap", derive(clap::Parser))]
13#[derive(Default)]
14pub struct CompileParams {}
15
16pub fn compile(mut project: ProjectDiscovered, _: CompileParams) -> Result<ProjectCompiled> {
17    let files = std::mem::take(&mut project.sources);
18    let source_tree = SourceTree::new(files, Some(project.root_path.clone()));
19
20    let mut project = parse_and_compile(&source_tree).map_err(|e| e.composed(&source_tree))?;
21
22    project.sources = source_tree;
23    Ok(project)
24}
25
26fn parse_and_compile(source_tree: &SourceTree) -> Result<ProjectCompiled, ErrorMessages> {
27    let options = Options::default()
28        .with_target(Target::Sql(Some(Dialect::SQLite)))
29        .no_format()
30        .no_signature();
31
32    // parse and resolve
33    let ast_tree = prqlc::prql_to_pl_tree(source_tree)?;
34    let mut root_module = semantic::resolve(ast_tree)?;
35
36    // find the database module
37    let database_module = find_database_module(&mut root_module)?;
38
39    // compile all main queries
40    let mut queries = HashMap::new();
41    let main_idents = root_module.find_mains();
42    for main_ident in main_idents {
43        let main_path: Vec<_> = main_ident.iter().cloned().collect();
44
45        let rq;
46        (rq, root_module) = semantic::lower_to_ir(root_module, &main_path, &database_module.path)?;
47        let sql = prqlc::rq_to_sql(rq, &options)?;
48
49        queries.insert(main_ident, sql);
50    }
51    Ok(ProjectCompiled {
52        sources: SourceTree::default(), // placeholder
53        queries,
54        database_module,
55        root_module,
56    })
57}
58
59fn find_database_module(root_module: &mut RootModule) -> Result<DatabaseModule, Errors> {
60    let lutra_sqlite = Ident::from_path(vec!["lutra", "sqlite"]);
61    let db_modules_fq = root_module.find_by_annotation_name(&lutra_sqlite);
62
63    let db_module_fq = match db_modules_fq.len() {
64        0 => {
65            return Err(Error::new_simple("cannot find the database module.")
66                .push_hint("define a module annotated with `@lutra.sqlite`")
67                .into());
68        }
69        1 => db_modules_fq.into_iter().next().unwrap(),
70        _ => {
71            return Err(Error::new_simple("cannot query multiple databases")
72                .push_hint("you can define only one module annotated with `@lutra.sqlite`")
73                .push_hint("this will be supported in the future")
74                .into());
75        }
76    };
77
78    // extract the declaration and retrieve its annotation
79    let decl = root_module.module.get(&db_module_fq).unwrap();
80    let annotation = decl
81        .annotations
82        .iter()
83        .find(|x| prqlc::semantic::is_ident_or_func_call(&x.expr, &lutra_sqlite))
84        .unwrap();
85
86    let def_id = decl.declared_at;
87
88    // make sure that there is exactly one arg
89    let arg = match &annotation.expr.kind {
90        prqlc::ir::pl::ExprKind::Ident(_) => {
91            return Err(Error::new_simple("missing connection parameters")
92                .push_hint("add `{file='sqlite.db'}`")
93                .with_span(annotation.expr.span)
94                .into());
95        }
96        prqlc::ir::pl::ExprKind::FuncCall(call) => {
97            // TODO: maybe this should be checked by actual type-checker
98            if call.args.len() != 1 {
99                Err(Error::new_simple("expected exactly one argument")
100                    .with_span(annotation.expr.span))?;
101            }
102            call.args.first().unwrap()
103        }
104        _ => unreachable!(),
105    };
106
107    let params = prqlc::semantic::static_eval(arg.clone(), root_module)?;
108    let prqlc::ir::constant::ConstExprKind::Tuple(params) = params.kind else {
109        return Err(Error::new_simple("expected exactly one argument")
110            .with_span(params.span)
111            .into());
112    };
113
114    let file = params.into_iter().next().unwrap();
115    let prqlc::ir::constant::ConstExprKind::Literal(Literal::String(file_str)) = file.kind else {
116        return Err(Error::new_simple("expected a string")
117            .with_span(file.span)
118            .into());
119    };
120
121    let file_relative = std::path::PathBuf::from_str(&file_str)
122        .map_err(|e| Error::new_simple(e.to_string()).with_span(file.span))?;
123    if !file_relative.is_relative() {
124        Err(
125            Error::new_simple("expected a relative path to the SQLite database file")
126                .with_span(file.span),
127        )?;
128    }
129
130    Ok(DatabaseModule {
131        path: db_module_fq.into_iter().collect(),
132        def_id,
133        connection_params: SqliteConnectionParams { file_relative },
134    })
135}