mod conflict;
mod params;
mod resolve;
mod results;
mod statements;
#[cfg(test)]
mod tests;
use std::collections::{BTreeMap, HashSet};
use std::ops::Deref;
use std::rc::Rc;
use heck::ToSnakeCase;
use sqlparser::dialect::PostgreSqlDialect;
use sqlparser::parser::Parser;
use self::conflict::take_conflict_predicate;
use self::results::result_shape;
use super::annotations::{QueryDef, QueryFailure, ReturnMode};
use crate::parser::TableFailure;
use crate::parser::naming;
use crate::schema::{EnumType, Table};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Param {
pub index: usize,
pub name: String,
pub rust_type: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputColumn {
pub sql_name: String,
pub rust_name: String,
pub rust_type: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResultShape {
None,
Model { module: String, struct_name: String },
Scalar(String),
Row(Vec<OutputColumn>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnalyzedQuery {
pub name: String,
pub fn_name: String,
pub mode: ReturnMode,
pub sql: String,
pub params: Vec<Param>,
pub result: ResultShape,
}
pub fn analyze_queries(
defs: &[QueryDef],
tables: &[Table],
enums: &[EnumType],
generated_modules: &HashSet<String>,
unsupported: &[TableFailure],
) -> (Vec<AnalyzedQuery>, Vec<QueryFailure>) {
let mut analyzed = Vec::new();
let mut failures = Vec::new();
let mut fn_names = HashSet::new();
for def in defs {
let context = Context {
tables,
enums,
unsupported,
generated_modules,
};
match analyze_one(def, &context) {
Ok(query) if !fn_names.insert(query.fn_name.clone()) => failures.push(
QueryFailure::new(
&def.name,
format!(
"function name `{}` is already used by another query",
query.fn_name
),
)
.at_line(def.line),
),
Ok(query) => analyzed.push(query),
Err(reason) => failures.push(QueryFailure::new(&def.name, reason).at_line(def.line)),
}
}
(analyzed, failures)
}
struct Context<'a> {
tables: &'a [Table],
enums: &'a [EnumType],
unsupported: &'a [TableFailure],
generated_modules: &'a HashSet<String>,
}
fn analyze_one(def: &QueryDef, context: &Context<'_>) -> Result<AnalyzedQuery, String> {
let fn_name = naming::rust_identifier(&def.name.to_snake_case())?;
let conflict = take_conflict_predicate(&def.sql)?;
let statements = Parser::parse_sql(&PostgreSqlDialect {}, &conflict.sql)
.map_err(|error| format!("invalid SQL: {error}"))?;
let statement = match statements.as_slice() {
[statement] => statement,
[] => return Err("the query has no SQL statement".to_owned()),
many => {
return Err(format!(
"the query contains {} statements; generate one function per statement and run them in a caller-owned transaction (`&mut *tx`)",
many.len()
));
}
};
let mut analysis = Analysis::new(context);
analysis.conflict_predicate = conflict.predicate;
let outputs = analysis.statement(statement)?;
let params = analysis.params(statement, def)?;
let result = result_shape(def, outputs, context)?;
Ok(AnalyzedQuery {
name: def.name.clone(),
fn_name,
mode: def.mode,
sql: def
.sql
.trim_end()
.trim_end_matches(';')
.trim_end()
.to_owned(),
params,
result,
})
}
struct ScopeEntry<'a> {
qualifier: String,
table: TableRef<'a>,
outer_nullable: bool,
}
struct Output {
sql_name: String,
rust_type: Option<String>,
origin: Option<(String, usize)>,
nullable_origin: bool,
unknown_reason: Option<String>,
name_error: Option<String>,
}
#[derive(Clone)]
enum TableRef<'a> {
Schema(&'a Table),
Cte(Rc<Table>),
}
impl TableRef<'_> {
fn module(&self) -> Option<&str> {
match self {
TableRef::Schema(table) => Some(&table.module_name),
TableRef::Cte(_) => None,
}
}
}
impl Deref for TableRef<'_> {
type Target = Table;
fn deref(&self) -> &Table {
match self {
TableRef::Schema(table) => table,
TableRef::Cte(table) => table,
}
}
}
struct Slot {
name: String,
base: String,
raw: String,
nullable: bool,
}
struct Analysis<'a, 'c> {
context: &'c Context<'a>,
scope: Vec<ScopeEntry<'a>>,
ctes: Vec<Rc<Table>>,
slots: BTreeMap<usize, Slot>,
conflict_predicate: Option<sqlparser::ast::Expr>,
}
impl<'a, 'c> Analysis<'a, 'c> {
fn new(context: &'c Context<'a>) -> Self {
Self {
context,
scope: Vec::new(),
ctes: Vec::new(),
slots: BTreeMap::new(),
conflict_predicate: None,
}
}
}