1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
use std::collections::HashMap;
use std::str::FromStr;

use anyhow::Result;
use prqlc::ir::decl::RootModule;
use prqlc::ir::pl::{Ident, Literal};
use prqlc::sql::Dialect;
use prqlc::{semantic, Error, ErrorMessages, Errors, Options, SourceTree, Target, WithErrorInfo};

use crate::project::{DatabaseModule, ProjectCompiled, ProjectDiscovered, SqliteConnectionParams};

#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[derive(Default)]
pub struct CompileParams {}

pub fn compile(mut project: ProjectDiscovered, _: CompileParams) -> Result<ProjectCompiled> {
    let files = std::mem::take(&mut project.sources);
    let source_tree = SourceTree::new(files, Some(project.root_path.clone()));

    let mut project = parse_and_compile(&source_tree).map_err(|e| e.composed(&source_tree))?;

    project.sources = source_tree;
    Ok(project)
}

fn parse_and_compile(source_tree: &SourceTree) -> Result<ProjectCompiled, ErrorMessages> {
    let options = Options::default()
        .with_target(Target::Sql(Some(Dialect::SQLite)))
        .no_format()
        .no_signature();

    // parse and resolve
    let ast_tree = prqlc::prql_to_pl_tree(source_tree)?;
    let mut root_module = semantic::resolve(ast_tree)?;

    // find the database module
    let database_module = find_database_module(&mut root_module)?;

    // compile all main queries
    let mut queries = HashMap::new();
    let main_idents = root_module.find_mains();
    for main_ident in main_idents {
        let main_path: Vec<_> = main_ident.iter().cloned().collect();

        let rq;
        (rq, root_module) = semantic::lower_to_ir(root_module, &main_path, &database_module.path)?;
        let sql = prqlc::rq_to_sql(rq, &options)?;

        queries.insert(main_ident, sql);
    }
    Ok(ProjectCompiled {
        sources: SourceTree::default(), // placeholder
        queries,
        database_module,
        root_module,
    })
}

fn find_database_module(root_module: &mut RootModule) -> Result<DatabaseModule, Errors> {
    let lutra_sqlite = Ident::from_path(vec!["lutra", "sqlite"]);
    let db_modules_fq = root_module.find_by_annotation_name(&lutra_sqlite);

    let db_module_fq = match db_modules_fq.len() {
        0 => {
            return Err(Error::new_simple("cannot find the database module.")
                .push_hint("define a module annotated with `@lutra.sqlite`")
                .into());
        }
        1 => db_modules_fq.into_iter().next().unwrap(),
        _ => {
            return Err(Error::new_simple("cannot query multiple databases")
                .push_hint("you can define only one module annotated with `@lutra.sqlite`")
                .push_hint("this will be supported in the future")
                .into());
        }
    };

    // extract the declaration and retrieve its annotation
    let decl = root_module.module.get(&db_module_fq).unwrap();
    let annotation = decl
        .annotations
        .iter()
        .find(|x| prqlc::semantic::is_ident_or_func_call(&x.expr, &lutra_sqlite))
        .unwrap();

    let def_id = decl.declared_at;

    // make sure that there is exactly one arg
    let arg = match &annotation.expr.kind {
        prqlc::ir::pl::ExprKind::Ident(_) => {
            return Err(Error::new_simple("missing connection parameters")
                .push_hint("add `{file='sqlite.db'}`")
                .with_span(annotation.expr.span)
                .into());
        }
        prqlc::ir::pl::ExprKind::FuncCall(call) => {
            // TODO: maybe this should be checked by actual type-checker
            if call.args.len() != 1 {
                Err(Error::new_simple("expected exactly one argument")
                    .with_span(annotation.expr.span))?;
            }
            call.args.first().unwrap()
        }
        _ => unreachable!(),
    };

    let params = prqlc::semantic::static_eval(arg.clone(), root_module)?;
    let prqlc::ir::constant::ConstExprKind::Tuple(params) = params.kind else {
        return Err(Error::new_simple("expected exactly one argument")
            .with_span(params.span)
            .into());
    };

    let file = params.into_iter().next().unwrap();
    let prqlc::ir::constant::ConstExprKind::Literal(Literal::String(file_str)) = file.kind else {
        return Err(Error::new_simple("expected a string")
            .with_span(file.span)
            .into());
    };

    let file_relative = std::path::PathBuf::from_str(&file_str)
        .map_err(|e| Error::new_simple(e.to_string()).with_span(file.span))?;
    if !file_relative.is_relative() {
        Err(
            Error::new_simple("expected a relative path to the SQLite database file")
                .with_span(file.span),
        )?;
    }

    Ok(DatabaseModule {
        path: db_module_fq.into_iter().collect(),
        def_id,
        connection_params: SqliteConnectionParams { file_relative },
    })
}