use strid::*;
mod expr;
pub use expr::*;
mod render;
pub use render::*;
mod stmt;
pub use stmt::*;
#[derive(Debug, Clone)]
pub struct RenderedSql {
pub sql: String,
pub params: Vec<ParamName>,
}
#[braid]
pub struct TableName;
#[braid]
pub struct ColumnName;
#[braid]
pub struct ParamName;
#[braid]
pub struct PgType;
pub struct Lit<T: AsRef<str>>(pub T);
impl<T: AsRef<str>> std::fmt::Display for Lit<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "'")?;
for c in self.0.as_ref().chars() {
if c == '\'' {
write!(f, "''")?;
} else {
write!(f, "{}", c)?;
}
}
write!(f, "'")
}
}
pub struct Ident<T: AsRef<str>>(pub T);
impl<T: AsRef<str>> std::fmt::Display for Ident<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"")?;
for c in self.0.as_ref().chars() {
if c == '"' {
write!(f, "\"\"")?;
} else {
write!(f, "{}", c)?;
}
}
write!(f, "\"")
}
}
pub fn escape_string(s: &str) -> String {
format!("{}", Lit(s))
}
pub fn quote_ident(name: &str) -> String {
format!("{}", Ident(name))
}
pub fn index_name(table: &str, columns: &[impl AsRef<str>]) -> String {
let cols: Vec<&str> = columns.iter().map(|c| c.as_ref()).collect();
format!("idx_{}_{}", table, cols.join("_"))
}
pub fn unique_index_name(table: &str, columns: &[impl AsRef<str>]) -> String {
let cols: Vec<&str> = columns.iter().map(|c| c.as_ref()).collect();
format!("uq_{}_{}", table, cols.join("_"))
}
pub fn check_constraint_name(table: &str, expr: &str) -> String {
let normalized = normalize_sql_expr_for_hash(expr);
let hex = blake3::hash(normalized.as_bytes()).to_hex().to_string();
let suffix = &hex[..16];
const PG_IDENT_MAX: usize = 63;
let prefix_overhead = "ck__".len(); let suffix_len = suffix.len();
let max_table_len = PG_IDENT_MAX.saturating_sub(prefix_overhead + suffix_len);
let table_part = if table.len() <= max_table_len {
table
} else {
let mut len = max_table_len.min(table.len());
while len > 0 && !table.is_char_boundary(len) {
len -= 1;
}
&table[..len]
};
format!("ck_{}_{}", table_part, suffix)
}
pub fn trigger_check_name(table: &str, expr: &str) -> String {
let normalized = normalize_sql_expr_for_hash(expr);
let hex = blake3::hash(normalized.as_bytes()).to_hex().to_string();
let suffix = &hex[..16];
const PG_IDENT_MAX: usize = 63;
let prefix_overhead = "trgck__".len(); let suffix_len = suffix.len();
let max_table_len = PG_IDENT_MAX.saturating_sub(prefix_overhead + suffix_len);
let table_part = if table.len() <= max_table_len {
table
} else {
let mut len = max_table_len.min(table.len());
while len > 0 && !table.is_char_boundary(len) {
len -= 1;
}
&table[..len]
};
format!("trgck_{}_{}", table_part, suffix)
}
pub fn trigger_check_function_name(trigger_name: &str) -> String {
let hex = blake3::hash(trigger_name.as_bytes()).to_hex().to_string();
format!("trgfn_{}", &hex[..20])
}
fn normalize_sql_expr_for_hash(expr: &str) -> String {
let mut out = String::with_capacity(expr.len());
let mut pending_space = false;
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut chars = expr.chars().peekable();
while let Some(ch) = chars.next() {
if in_single_quote {
out.push(ch);
if ch == '\'' {
if matches!(chars.peek(), Some('\'')) {
out.push(chars.next().expect("peeked"));
} else {
in_single_quote = false;
}
}
continue;
}
if in_double_quote {
out.push(ch);
if ch == '"' {
if matches!(chars.peek(), Some('"')) {
out.push(chars.next().expect("peeked"));
} else {
in_double_quote = false;
}
}
continue;
}
match ch {
'\'' => {
if pending_space && !out.is_empty() {
out.push(' ');
}
pending_space = false;
out.push('\'');
in_single_quote = true;
}
'"' => {
if pending_space && !out.is_empty() {
out.push(' ');
}
pending_space = false;
out.push('"');
in_double_quote = true;
}
c if c.is_whitespace() => {
pending_space = true;
}
c => {
if pending_space && !out.is_empty() {
out.push(' ');
}
pending_space = false;
out.push(c);
}
}
}
out.trim().to_string()
}