use postgres_types::Type;
use quote::format_ident;
use syn::parse_quote;
pub struct Parameter {
pub r#type: Type,
pub is_optional: bool,
pub number: usize,
}
impl Parameter {
pub fn to_syn_type(&self) -> syn::Type {
let raw_type = match &self.r#type {
&Type::BOOL => parse_quote!(&'a bool),
&Type::BOOL_ARRAY => parse_quote!(&'a [bool]),
&Type::BYTEA => parse_quote!(&'a [u8]),
&Type::BYTEA_ARRAY => parse_quote!(&'a [Vec<u8>]),
&Type::CHAR => parse_quote!(&'a i8),
&Type::CHAR_ARRAY => parse_quote!(&'a [i8]),
&Type::INT8 => parse_quote!(&'a i64),
&Type::INT8_ARRAY => parse_quote!(&'a [i64]),
&Type::INT4 => parse_quote!(&'a i32),
&Type::INT4_ARRAY => parse_quote!(&'a [i32]),
&Type::INT2 => parse_quote!(&'a i16),
&Type::INT2_ARRAY => parse_quote!(&'a [i16]),
&Type::FLOAT8 => parse_quote!(&'a f64),
&Type::FLOAT8_ARRAY => parse_quote!(&'a [f64]),
&Type::FLOAT4 => parse_quote!(&'a f32),
&Type::FLOAT4_ARRAY => parse_quote!(&'a [f32]),
&Type::UUID => parse_quote!(&'a uuid::Uuid),
&Type::UUID_ARRAY => parse_quote!(&'a [uuid::Uuid]),
&Type::TEXT | &Type::VARCHAR => parse_quote!(&'a str),
&Type::VARCHAR_ARRAY | &Type::TEXT_ARRAY => parse_quote!(&'a [String]),
&Type::TIMESTAMP => parse_quote!(&'a ts_sql_helper_lib::SqlDateTime),
&Type::TIMESTAMP_ARRAY => parse_quote!(&'a [ts_sql_helper_lib::SqlDateTime]),
&Type::TIMESTAMPTZ => parse_quote!(&'a ts_sql_helper_lib::SqlTimestamp),
&Type::TIMESTAMPTZ_ARRAY => parse_quote!(&'a [ts_sql_helper_lib::SqlTimestamp]),
&Type::DATE => parse_quote!(&'a ts_sql_helper_lib::SqlDate),
&Type::DATE_ARRAY => parse_quote!(&'a [ts_sql_helper_lib::SqlDate]),
&Type::TIME => parse_quote!(&'a ts_sql_helper_lib::SqlTime),
&Type::TIME_ARRAY => parse_quote!(&'a [ts_sql_helper_lib::SqlTime]),
_ => unreachable!(),
};
if self.is_optional {
parse_quote!(Option<#raw_type>)
} else {
raw_type
}
}
pub fn name(&self) -> syn::Ident {
format_ident!("p{}", self.number)
}
}
pub fn get_parameters(sql: &str, optional_parameters: Vec<usize>) -> Vec<Parameter> {
let mut parameter_types: Vec<(usize, String)> = vec![];
enum State {
Neutral,
ConsumingNumber { number: String },
ConsumingTypeSeparator { number: usize },
ConsumingType { number: usize, type_string: String },
}
let mut state = State::Neutral;
for character in sql.chars() {
match &mut state {
State::Neutral => {
if character == '$' {
state = State::ConsumingNumber {
number: String::new(),
};
}
}
State::ConsumingNumber { number } => {
if character.is_ascii_digit() {
number.push(character);
} else if character == ':' {
state = State::ConsumingTypeSeparator {
number: number.parse().expect("variable should be a number"),
};
} else {
if !number.is_empty() {
panic!("variable {number} must have a type");
}
state = State::Neutral;
}
}
State::ConsumingTypeSeparator { number } => {
if character.is_ascii_alphabetic() {
state = State::ConsumingType {
number: *number,
type_string: character.to_string(),
};
} else if character != ':' {
panic!("variable {number} must have a type");
}
}
State::ConsumingType {
number,
type_string,
} => {
if character.is_ascii_alphanumeric() || character == '[' || character == ']' {
type_string.push(character);
} else if type_string.is_empty() {
panic!("variable {number} must have a type");
} else {
parameter_types.push((*number, type_string.to_uppercase()));
state = State::Neutral;
}
}
}
}
match state {
State::Neutral => {}
State::ConsumingNumber { number } => {
if !number.is_empty() {
panic!("variable {number} must have a type");
}
}
State::ConsumingTypeSeparator { number } => {
panic!("variable {number} must have a type");
}
State::ConsumingType {
number,
type_string,
} => {
if type_string.is_empty() {
panic!("variable {number} must have a type");
} else {
parameter_types.push((number, type_string.to_uppercase()));
}
}
}
parameter_types.sort_by_key(|v| v.0);
parameter_types.dedup();
let mut seen_variables = vec![];
for (number, _) in parameter_types.iter() {
if seen_variables.contains(number) {
panic!("variable {number} is defined with multiple types")
}
if let Some(variable) = seen_variables.last()
&& number - 1 != *variable
{
panic!("variable {} is missing", number - 1)
}
seen_variables.push(*number);
}
parameter_types
.into_iter()
.map(|(number, type_string)| {
let r#type = match type_string.as_str() {
"BOOL" => Type::BOOL,
"BOOL[]" => Type::BOOL_ARRAY,
"BYTEA" => Type::BYTEA,
"BYTEA[]" => Type::BYTEA_ARRAY,
"CHAR" => Type::CHAR,
"CHAR[]" => Type::CHAR_ARRAY,
"INT8" => Type::INT8,
"INT8[]" => Type::INT8_ARRAY,
"INT4" => Type::INT4,
"INT4[]" => Type::INT4_ARRAY,
"INT2" => Type::INT2,
"INT2[]" => Type::INT2_ARRAY,
"FLOAT8" => Type::FLOAT8,
"FLOAT8[]" => Type::FLOAT8_ARRAY,
"FLOAT4" => Type::FLOAT4,
"FLOAT4[]" => Type::FLOAT4_ARRAY,
"UUID" => Type::UUID,
"UUID[]" => Type::UUID_ARRAY,
"TEXT" => Type::TEXT,
"VARCHAR" => Type::VARCHAR,
"VARCHAR[]" => Type::VARCHAR_ARRAY,
"TEXT[]" => Type::TEXT_ARRAY,
"TIMESTAMP" => Type::TIMESTAMP,
"TIMESTAMP[]" => Type::TIMESTAMP_ARRAY,
"TIMESTAMPTZ" => Type::TIMESTAMPTZ,
"TIMESTAMPTZ[]" => Type::TIMESTAMPTZ_ARRAY,
"DATE" => Type::DATE,
"DATE[]" => Type::DATE_ARRAY,
"TIME" => Type::TIME,
"TIME[]" => Type::TIME_ARRAY,
type_string => {
panic!("unsupported type `{type_string}`")
}
};
Parameter {
r#type,
is_optional: optional_parameters.contains(&number),
number,
}
})
.collect()
}