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