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 let ast_tree = prqlc::prql_to_pl_tree(source_tree)?;
34 let mut root_module = semantic::resolve(ast_tree)?;
35
36 let database_module = find_database_module(&mut root_module)?;
38
39 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(), 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 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 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 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}