use crate::filter::FilterValue;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Write;
use std::sync::{Arc, OnceLock, RwLock};
use tracing::debug;
pub mod keywords {
pub const SELECT: &str = "SELECT";
pub const INSERT: &str = "INSERT";
pub const UPDATE: &str = "UPDATE";
pub const DELETE: &str = "DELETE";
pub const INTO: &str = "INTO";
pub const VALUES: &str = "VALUES";
pub const SET: &str = "SET";
pub const FROM: &str = "FROM";
pub const WHERE: &str = "WHERE";
pub const RETURNING: &str = "RETURNING";
pub const AND: &str = "AND";
pub const OR: &str = "OR";
pub const NOT: &str = "NOT";
pub const IN: &str = "IN";
pub const IS: &str = "IS";
pub const NULL: &str = "NULL";
pub const LIKE: &str = "LIKE";
pub const ILIKE: &str = "ILIKE";
pub const BETWEEN: &str = "BETWEEN";
pub const EXISTS: &str = "EXISTS";
pub const ORDER_BY: &str = "ORDER BY";
pub const ASC: &str = "ASC";
pub const DESC: &str = "DESC";
pub const NULLS_FIRST: &str = "NULLS FIRST";
pub const NULLS_LAST: &str = "NULLS LAST";
pub const LIMIT: &str = "LIMIT";
pub const OFFSET: &str = "OFFSET";
pub const GROUP_BY: &str = "GROUP BY";
pub const HAVING: &str = "HAVING";
pub const DISTINCT: &str = "DISTINCT";
pub const DISTINCT_ON: &str = "DISTINCT ON";
pub const JOIN: &str = "JOIN";
pub const INNER_JOIN: &str = "INNER JOIN";
pub const LEFT_JOIN: &str = "LEFT JOIN";
pub const RIGHT_JOIN: &str = "RIGHT JOIN";
pub const FULL_JOIN: &str = "FULL OUTER JOIN";
pub const CROSS_JOIN: &str = "CROSS JOIN";
pub const LATERAL: &str = "LATERAL";
pub const ON: &str = "ON";
pub const USING: &str = "USING";
pub const WITH: &str = "WITH";
pub const RECURSIVE: &str = "RECURSIVE";
pub const AS: &str = "AS";
pub const MATERIALIZED: &str = "MATERIALIZED";
pub const NOT_MATERIALIZED: &str = "NOT MATERIALIZED";
pub const OVER: &str = "OVER";
pub const PARTITION_BY: &str = "PARTITION BY";
pub const ROWS: &str = "ROWS";
pub const RANGE: &str = "RANGE";
pub const GROUPS: &str = "GROUPS";
pub const UNBOUNDED_PRECEDING: &str = "UNBOUNDED PRECEDING";
pub const UNBOUNDED_FOLLOWING: &str = "UNBOUNDED FOLLOWING";
pub const CURRENT_ROW: &str = "CURRENT ROW";
pub const PRECEDING: &str = "PRECEDING";
pub const FOLLOWING: &str = "FOLLOWING";
pub const COUNT: &str = "COUNT";
pub const SUM: &str = "SUM";
pub const AVG: &str = "AVG";
pub const MIN: &str = "MIN";
pub const MAX: &str = "MAX";
pub const ROW_NUMBER: &str = "ROW_NUMBER";
pub const RANK: &str = "RANK";
pub const DENSE_RANK: &str = "DENSE_RANK";
pub const LAG: &str = "LAG";
pub const LEAD: &str = "LEAD";
pub const FIRST_VALUE: &str = "FIRST_VALUE";
pub const LAST_VALUE: &str = "LAST_VALUE";
pub const NTILE: &str = "NTILE";
pub const ON_CONFLICT: &str = "ON CONFLICT";
pub const DO_NOTHING: &str = "DO NOTHING";
pub const DO_UPDATE: &str = "DO UPDATE";
pub const EXCLUDED: &str = "excluded";
pub const ON_DUPLICATE_KEY: &str = "ON DUPLICATE KEY UPDATE";
pub const MERGE: &str = "MERGE";
pub const MATCHED: &str = "MATCHED";
pub const NOT_MATCHED: &str = "NOT MATCHED";
pub const FOR_UPDATE: &str = "FOR UPDATE";
pub const FOR_SHARE: &str = "FOR SHARE";
pub const NOWAIT: &str = "NOWAIT";
pub const SKIP_LOCKED: &str = "SKIP LOCKED";
pub const CREATE: &str = "CREATE";
pub const ALTER: &str = "ALTER";
pub const DROP: &str = "DROP";
pub const TABLE: &str = "TABLE";
pub const INDEX: &str = "INDEX";
pub const VIEW: &str = "VIEW";
pub const TRIGGER: &str = "TRIGGER";
pub const FUNCTION: &str = "FUNCTION";
pub const PROCEDURE: &str = "PROCEDURE";
pub const SEQUENCE: &str = "SEQUENCE";
pub const IF_EXISTS: &str = "IF EXISTS";
pub const IF_NOT_EXISTS: &str = "IF NOT EXISTS";
pub const OR_REPLACE: &str = "OR REPLACE";
pub const CASCADE: &str = "CASCADE";
pub const RESTRICT: &str = "RESTRICT";
pub const PRIMARY_KEY: &str = "PRIMARY KEY";
pub const FOREIGN_KEY: &str = "FOREIGN KEY";
pub const REFERENCES: &str = "REFERENCES";
pub const UNIQUE: &str = "UNIQUE";
pub const CHECK: &str = "CHECK";
pub const DEFAULT: &str = "DEFAULT";
pub const NOT_NULL: &str = "NOT NULL";
pub const SPACE: &str = " ";
pub const COMMA_SPACE: &str = ", ";
pub const OPEN_PAREN: &str = "(";
pub const CLOSE_PAREN: &str = ")";
pub const STAR: &str = "*";
pub const EQUALS: &str = " = ";
pub const NOT_EQUALS: &str = " <> ";
pub const LESS_THAN: &str = " < ";
pub const GREATER_THAN: &str = " > ";
pub const LESS_OR_EQUAL: &str = " <= ";
pub const GREATER_OR_EQUAL: &str = " >= ";
}
pub fn escape_identifier(name: &str) -> String {
let escaped = name.replace('"', "\"\"");
format!("\"{}\"", escaped)
}
pub fn needs_quoting(name: &str) -> bool {
let reserved = [
"user",
"order",
"group",
"select",
"from",
"where",
"table",
"index",
"key",
"primary",
"foreign",
"check",
"default",
"null",
"not",
"and",
"or",
"in",
"is",
"like",
"between",
"case",
"when",
"then",
"else",
"end",
"as",
"on",
"join",
"left",
"right",
"inner",
"outer",
"cross",
"natural",
"using",
"limit",
"offset",
"union",
"intersect",
"except",
"all",
"distinct",
"having",
"create",
"alter",
"drop",
"insert",
"update",
"delete",
"into",
"values",
"set",
"returning",
];
if reserved.contains(&name.to_lowercase().as_str()) {
return true;
}
!name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub fn quote_identifier(name: &str) -> String {
if needs_quoting(name) {
escape_identifier(name)
} else {
name.to_string()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum DatabaseType {
#[default]
PostgreSQL,
MySQL,
SQLite,
MSSQL,
}
const QUESTION_MARK_PLACEHOLDER: &str = "?";
pub const POSTGRES_PLACEHOLDERS: &[&str] = &[
"$0", "$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9", "$10", "$11", "$12", "$13", "$14",
"$15", "$16", "$17", "$18", "$19", "$20", "$21", "$22", "$23", "$24", "$25", "$26", "$27",
"$28", "$29", "$30", "$31", "$32", "$33", "$34", "$35", "$36", "$37", "$38", "$39", "$40",
"$41", "$42", "$43", "$44", "$45", "$46", "$47", "$48", "$49", "$50", "$51", "$52", "$53",
"$54", "$55", "$56", "$57", "$58", "$59", "$60", "$61", "$62", "$63", "$64", "$65", "$66",
"$67", "$68", "$69", "$70", "$71", "$72", "$73", "$74", "$75", "$76", "$77", "$78", "$79",
"$80", "$81", "$82", "$83", "$84", "$85", "$86", "$87", "$88", "$89", "$90", "$91", "$92",
"$93", "$94", "$95", "$96", "$97", "$98", "$99", "$100", "$101", "$102", "$103", "$104",
"$105", "$106", "$107", "$108", "$109", "$110", "$111", "$112", "$113", "$114", "$115", "$116",
"$117", "$118", "$119", "$120", "$121", "$122", "$123", "$124", "$125", "$126", "$127", "$128",
"$129", "$130", "$131", "$132", "$133", "$134", "$135", "$136", "$137", "$138", "$139", "$140",
"$141", "$142", "$143", "$144", "$145", "$146", "$147", "$148", "$149", "$150", "$151", "$152",
"$153", "$154", "$155", "$156", "$157", "$158", "$159", "$160", "$161", "$162", "$163", "$164",
"$165", "$166", "$167", "$168", "$169", "$170", "$171", "$172", "$173", "$174", "$175", "$176",
"$177", "$178", "$179", "$180", "$181", "$182", "$183", "$184", "$185", "$186", "$187", "$188",
"$189", "$190", "$191", "$192", "$193", "$194", "$195", "$196", "$197", "$198", "$199", "$200",
"$201", "$202", "$203", "$204", "$205", "$206", "$207", "$208", "$209", "$210", "$211", "$212",
"$213", "$214", "$215", "$216", "$217", "$218", "$219", "$220", "$221", "$222", "$223", "$224",
"$225", "$226", "$227", "$228", "$229", "$230", "$231", "$232", "$233", "$234", "$235", "$236",
"$237", "$238", "$239", "$240", "$241", "$242", "$243", "$244", "$245", "$246", "$247", "$248",
"$249", "$250", "$251", "$252", "$253", "$254", "$255", "$256",
];
pub const MYSQL_IN_PATTERNS: &[&str] = &[
"", "?",
"?, ?",
"?, ?, ?",
"?, ?, ?, ?",
"?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", ];
#[inline]
pub fn postgres_in_pattern(start_idx: usize, count: usize) -> String {
if start_idx == 1 && count <= 10 {
static POSTGRES_IN_1: &[&str] = &[
"",
"$1",
"$1, $2",
"$1, $2, $3",
"$1, $2, $3, $4",
"$1, $2, $3, $4, $5",
"$1, $2, $3, $4, $5, $6",
"$1, $2, $3, $4, $5, $6, $7",
"$1, $2, $3, $4, $5, $6, $7, $8",
"$1, $2, $3, $4, $5, $6, $7, $8, $9",
"$1, $2, $3, $4, $5, $6, $7, $8, $9, $10",
];
return POSTGRES_IN_1[count].to_string();
}
let mut result = String::with_capacity(count * 5);
for i in 0..count {
if i > 0 {
result.push_str(", ");
}
let idx = start_idx + i;
if idx < POSTGRES_PLACEHOLDERS.len() {
result.push_str(POSTGRES_PLACEHOLDERS[idx]);
} else {
use std::fmt::Write;
let _ = write!(result, "${}", idx);
}
}
result
}
const POSTGRES_IN_FROM_1: &[&str] = &[
"", "$1", "$1, $2", "$1, $2, $3", "$1, $2, $3, $4", "$1, $2, $3, $4, $5", "$1, $2, $3, $4, $5, $6", "$1, $2, $3, $4, $5, $6, $7", "$1, $2, $3, $4, $5, $6, $7, $8", "$1, $2, $3, $4, $5, $6, $7, $8, $9", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32", ];
#[inline]
#[allow(clippy::needless_range_loop)]
pub fn write_postgres_in_pattern(buf: &mut String, start_idx: usize, count: usize) {
if count == 0 {
return;
}
if start_idx == 1 && count < POSTGRES_IN_FROM_1.len() {
buf.push_str(POSTGRES_IN_FROM_1[count]);
return;
}
buf.reserve(count * 6);
let end_idx = start_idx + count;
let table_len = POSTGRES_PLACEHOLDERS.len();
if end_idx <= table_len {
buf.push_str(POSTGRES_PLACEHOLDERS[start_idx]);
for idx in (start_idx + 1)..end_idx {
buf.push_str(", ");
buf.push_str(POSTGRES_PLACEHOLDERS[idx]);
}
} else if start_idx >= table_len {
let _ = write!(buf, "${}", start_idx);
for idx in (start_idx + 1)..end_idx {
let _ = write!(buf, ", ${}", idx);
}
} else {
buf.push_str(POSTGRES_PLACEHOLDERS[start_idx]);
for idx in (start_idx + 1)..table_len.min(end_idx) {
buf.push_str(", ");
buf.push_str(POSTGRES_PLACEHOLDERS[idx]);
}
for idx in table_len..end_idx {
let _ = write!(buf, ", ${}", idx);
}
}
}
impl DatabaseType {
#[inline]
pub fn placeholder(&self, index: usize) -> Cow<'static, str> {
match self {
Self::PostgreSQL => {
if index > 0 && index < POSTGRES_PLACEHOLDERS.len() {
Cow::Borrowed(POSTGRES_PLACEHOLDERS[index])
} else {
Cow::Owned(format!("${}", index))
}
}
Self::MySQL | Self::SQLite => Cow::Borrowed(QUESTION_MARK_PLACEHOLDER),
Self::MSSQL => Cow::Owned(format!("@P{}", index)),
}
}
#[inline]
pub fn placeholder_string(&self, index: usize) -> String {
self.placeholder(index).into_owned()
}
}
#[derive(Debug, Clone)]
pub struct SqlBuilder {
db_type: DatabaseType,
parts: Vec<String>,
params: Vec<FilterValue>,
}
impl SqlBuilder {
pub fn new(db_type: DatabaseType) -> Self {
Self {
db_type,
parts: Vec::new(),
params: Vec::new(),
}
}
pub fn postgres() -> Self {
Self::new(DatabaseType::PostgreSQL)
}
pub fn mysql() -> Self {
Self::new(DatabaseType::MySQL)
}
pub fn sqlite() -> Self {
Self::new(DatabaseType::SQLite)
}
pub fn push(&mut self, sql: impl AsRef<str>) -> &mut Self {
self.parts.push(sql.as_ref().to_string());
self
}
pub fn push_param(&mut self, value: impl Into<FilterValue>) -> &mut Self {
let index = self.params.len() + 1;
self.parts
.push(self.db_type.placeholder(index).into_owned());
self.params.push(value.into());
self
}
pub fn push_identifier(&mut self, name: &str) -> &mut Self {
self.parts.push(quote_identifier(name));
self
}
pub fn push_sep(&mut self, sep: &str) -> &mut Self {
self.parts.push(sep.to_string());
self
}
pub fn build(self) -> (String, Vec<FilterValue>) {
(self.parts.join(""), self.params)
}
pub fn sql(&self) -> String {
self.parts.join("")
}
pub fn params(&self) -> &[FilterValue] {
&self.params
}
pub fn next_param_index(&self) -> usize {
self.params.len() + 1
}
}
impl Default for SqlBuilder {
fn default() -> Self {
Self::postgres()
}
}
#[derive(Debug, Clone, Copy)]
pub enum QueryCapacity {
SimpleSelect,
SelectWithFilters(usize),
Insert(usize),
Update(usize),
Delete,
Custom(usize),
}
impl QueryCapacity {
#[inline]
pub const fn estimate(&self) -> usize {
match self {
Self::SimpleSelect => 64,
Self::SelectWithFilters(n) => 64 + *n * 32,
Self::Insert(cols) => 32 + *cols * 16,
Self::Update(cols) => 32 + *cols * 20,
Self::Delete => 48,
Self::Custom(cap) => *cap,
}
}
}
#[derive(Debug, Clone)]
pub struct FastSqlBuilder {
buffer: String,
params: Vec<FilterValue>,
db_type: DatabaseType,
}
impl FastSqlBuilder {
#[inline]
pub fn new(db_type: DatabaseType) -> Self {
Self {
buffer: String::new(),
params: Vec::new(),
db_type,
}
}
#[inline]
pub fn with_capacity(db_type: DatabaseType, capacity: QueryCapacity) -> Self {
Self {
buffer: String::with_capacity(capacity.estimate()),
params: Vec::with_capacity(match capacity {
QueryCapacity::SimpleSelect => 2,
QueryCapacity::SelectWithFilters(n) => n,
QueryCapacity::Insert(n) => n,
QueryCapacity::Update(n) => n + 1,
QueryCapacity::Delete => 2,
QueryCapacity::Custom(n) => n / 16,
}),
db_type,
}
}
#[inline]
pub fn postgres(capacity: QueryCapacity) -> Self {
Self::with_capacity(DatabaseType::PostgreSQL, capacity)
}
#[inline]
pub fn mysql(capacity: QueryCapacity) -> Self {
Self::with_capacity(DatabaseType::MySQL, capacity)
}
#[inline]
pub fn sqlite(capacity: QueryCapacity) -> Self {
Self::with_capacity(DatabaseType::SQLite, capacity)
}
#[inline]
pub fn push_str(&mut self, s: &str) -> &mut Self {
self.buffer.push_str(s);
self
}
#[inline]
pub fn push_char(&mut self, c: char) -> &mut Self {
self.buffer.push(c);
self
}
#[inline]
pub fn bind(&mut self, value: impl Into<FilterValue>) -> &mut Self {
let index = self.params.len() + 1;
let placeholder = self.db_type.placeholder(index);
self.buffer.push_str(&placeholder);
self.params.push(value.into());
self
}
#[inline]
pub fn push_bind(&mut self, s: &str, value: impl Into<FilterValue>) -> &mut Self {
self.push_str(s);
self.bind(value)
}
pub fn bind_in_clause(&mut self, values: impl IntoIterator<Item = FilterValue>) -> &mut Self {
let values: Vec<FilterValue> = values.into_iter().collect();
if values.is_empty() {
return self;
}
let start_index = self.params.len() + 1;
let count = values.len();
match self.db_type {
DatabaseType::PostgreSQL => {
let estimated_len = count * 5;
self.buffer.reserve(estimated_len);
for (i, _) in values.iter().enumerate() {
if i > 0 {
self.buffer.push_str(", ");
}
let idx = start_index + i;
if idx < POSTGRES_PLACEHOLDERS.len() {
self.buffer.push_str(POSTGRES_PLACEHOLDERS[idx]);
} else {
let _ = write!(self.buffer, "${}", idx);
}
}
}
DatabaseType::MySQL | DatabaseType::SQLite => {
if start_index == 1 && count < MYSQL_IN_PATTERNS.len() {
self.buffer.push_str(MYSQL_IN_PATTERNS[count]);
} else {
let estimated_len = count * 3; self.buffer.reserve(estimated_len);
for i in 0..count {
if i > 0 {
self.buffer.push_str(", ");
}
self.buffer.push('?');
}
}
}
DatabaseType::MSSQL => {
let estimated_len = count * 6; self.buffer.reserve(estimated_len);
for (i, _) in values.iter().enumerate() {
if i > 0 {
self.buffer.push_str(", ");
}
let idx = start_index + i;
let _ = write!(self.buffer, "@P{}", idx);
}
}
}
self.params.extend(values);
self
}
pub fn bind_in_slice<T: Into<FilterValue> + Clone>(&mut self, values: &[T]) -> &mut Self {
if values.is_empty() {
return self;
}
let start_index = self.params.len() + 1;
let count = values.len();
match self.db_type {
DatabaseType::PostgreSQL => {
let estimated_len = count * 5;
self.buffer.reserve(estimated_len);
for i in 0..count {
if i > 0 {
self.buffer.push_str(", ");
}
let idx = start_index + i;
if idx < POSTGRES_PLACEHOLDERS.len() {
self.buffer.push_str(POSTGRES_PLACEHOLDERS[idx]);
} else {
let _ = write!(self.buffer, "${}", idx);
}
}
}
DatabaseType::MySQL | DatabaseType::SQLite => {
if start_index == 1 && count < MYSQL_IN_PATTERNS.len() {
self.buffer.push_str(MYSQL_IN_PATTERNS[count]);
} else {
let estimated_len = count * 3;
self.buffer.reserve(estimated_len);
for i in 0..count {
if i > 0 {
self.buffer.push_str(", ");
}
self.buffer.push('?');
}
}
}
DatabaseType::MSSQL => {
let estimated_len = count * 6;
self.buffer.reserve(estimated_len);
for i in 0..count {
if i > 0 {
self.buffer.push_str(", ");
}
let idx = start_index + i;
let _ = write!(self.buffer, "@P{}", idx);
}
}
}
self.params.reserve(count);
for v in values {
self.params.push(v.clone().into());
}
self
}
#[inline]
pub fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> &mut Self {
let _ = self.buffer.write_fmt(args);
self
}
#[inline]
pub fn push_identifier(&mut self, name: &str) -> &mut Self {
if needs_quoting(name) {
self.buffer.push('"');
for c in name.chars() {
if c == '"' {
self.buffer.push_str("\"\"");
} else {
self.buffer.push(c);
}
}
self.buffer.push('"');
} else {
self.buffer.push_str(name);
}
self
}
#[inline]
pub fn push_if(&mut self, condition: bool, s: &str) -> &mut Self {
if condition {
self.push_str(s);
}
self
}
#[inline]
pub fn bind_if(&mut self, condition: bool, value: impl Into<FilterValue>) -> &mut Self {
if condition {
self.bind(value);
}
self
}
#[inline]
pub fn sql(&self) -> &str {
&self.buffer
}
#[inline]
pub fn params(&self) -> &[FilterValue] {
&self.params
}
#[inline]
pub fn param_count(&self) -> usize {
self.params.len()
}
#[inline]
pub fn build(self) -> (String, Vec<FilterValue>) {
let sql_len = self.buffer.len();
let param_count = self.params.len();
debug!(sql_len, param_count, db_type = ?self.db_type, "FastSqlBuilder::build()");
(self.buffer, self.params)
}
#[inline]
pub fn build_sql(self) -> String {
self.buffer
}
}
pub mod templates {
use super::*;
pub fn select_by_id(table: &str, columns: &[&str]) -> String {
let cols = if columns.is_empty() {
"*".to_string()
} else {
columns.join(", ")
};
format!("SELECT {} FROM {} WHERE id = $1", cols, table)
}
pub fn insert_returning(table: &str, columns: &[&str]) -> String {
let cols = columns.join(", ");
let placeholders: Vec<String> = (1..=columns.len())
.map(|i| {
if i < POSTGRES_PLACEHOLDERS.len() {
POSTGRES_PLACEHOLDERS[i].to_string()
} else {
format!("${}", i)
}
})
.collect();
format!(
"INSERT INTO {} ({}) VALUES ({}) RETURNING *",
table,
cols,
placeholders.join(", ")
)
}
pub fn update_by_id(table: &str, columns: &[&str]) -> String {
let sets: Vec<String> = columns
.iter()
.enumerate()
.map(|(i, col)| {
let idx = i + 1;
if idx < POSTGRES_PLACEHOLDERS.len() {
format!("{} = {}", col, POSTGRES_PLACEHOLDERS[idx])
} else {
format!("{} = ${}", col, idx)
}
})
.collect();
let id_idx = columns.len() + 1;
let id_placeholder = if id_idx < POSTGRES_PLACEHOLDERS.len() {
POSTGRES_PLACEHOLDERS[id_idx]
} else {
"$?"
};
format!(
"UPDATE {} SET {} WHERE id = {}",
table,
sets.join(", "),
id_placeholder
)
}
pub fn delete_by_id(table: &str) -> String {
format!("DELETE FROM {} WHERE id = $1", table)
}
pub fn batch_placeholders(db_type: DatabaseType, columns: usize, rows: usize) -> String {
let mut result = String::with_capacity(rows * columns * 4);
let mut param_idx = 1;
for row in 0..rows {
if row > 0 {
result.push_str(", ");
}
result.push('(');
for col in 0..columns {
if col > 0 {
result.push_str(", ");
}
match db_type {
DatabaseType::PostgreSQL => {
if param_idx < POSTGRES_PLACEHOLDERS.len() {
result.push_str(POSTGRES_PLACEHOLDERS[param_idx]);
} else {
let _ = write!(result, "${}", param_idx);
}
param_idx += 1;
}
DatabaseType::MySQL | DatabaseType::SQLite => {
result.push('?');
}
DatabaseType::MSSQL => {
let _ = write!(result, "@P{}", param_idx);
param_idx += 1;
}
}
}
result.push(')');
}
result
}
}
pub struct LazySql<F>
where
F: Fn(DatabaseType) -> String,
{
generator: F,
}
impl<F> LazySql<F>
where
F: Fn(DatabaseType) -> String,
{
#[inline]
pub const fn new(generator: F) -> Self {
Self { generator }
}
#[inline]
pub fn get(&self, db_type: DatabaseType) -> String {
(self.generator)(db_type)
}
}
pub struct CachedSql<F>
where
F: Fn(DatabaseType) -> String,
{
generator: F,
postgres: OnceLock<String>,
mysql: OnceLock<String>,
sqlite: OnceLock<String>,
mssql: OnceLock<String>,
}
impl<F> CachedSql<F>
where
F: Fn(DatabaseType) -> String,
{
pub const fn new(generator: F) -> Self {
Self {
generator,
postgres: OnceLock::new(),
mysql: OnceLock::new(),
sqlite: OnceLock::new(),
mssql: OnceLock::new(),
}
}
pub fn get(&self, db_type: DatabaseType) -> &str {
match db_type {
DatabaseType::PostgreSQL => self.postgres.get_or_init(|| (self.generator)(db_type)),
DatabaseType::MySQL => self.mysql.get_or_init(|| (self.generator)(db_type)),
DatabaseType::SQLite => self.sqlite.get_or_init(|| (self.generator)(db_type)),
DatabaseType::MSSQL => self.mssql.get_or_init(|| (self.generator)(db_type)),
}
}
}
pub struct SqlTemplateCache {
cache: RwLock<HashMap<(String, DatabaseType), Arc<String>>>,
}
impl Default for SqlTemplateCache {
fn default() -> Self {
Self::new()
}
}
impl SqlTemplateCache {
pub fn new() -> Self {
Self {
cache: RwLock::new(HashMap::new()),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
cache: RwLock::new(HashMap::with_capacity(capacity)),
}
}
pub fn get_or_insert<F>(&self, key: &str, db_type: DatabaseType, generator: F) -> Arc<String>
where
F: FnOnce() -> String,
{
let cache_key = (key.to_string(), db_type);
{
let cache = self.cache.read().unwrap();
if let Some(sql) = cache.get(&cache_key) {
return Arc::clone(sql);
}
}
let mut cache = self.cache.write().unwrap();
if let Some(sql) = cache.get(&cache_key) {
return Arc::clone(sql);
}
let sql = Arc::new(generator());
cache.insert(cache_key, Arc::clone(&sql));
sql
}
pub fn contains(&self, key: &str, db_type: DatabaseType) -> bool {
let cache_key = (key.to_string(), db_type);
self.cache.read().unwrap().contains_key(&cache_key)
}
pub fn clear(&self) {
self.cache.write().unwrap().clear();
}
pub fn len(&self) -> usize {
self.cache.read().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.cache.read().unwrap().is_empty()
}
}
pub fn global_sql_cache() -> &'static SqlTemplateCache {
static CACHE: OnceLock<SqlTemplateCache> = OnceLock::new();
CACHE.get_or_init(|| SqlTemplateCache::with_capacity(64))
}
#[derive(Debug, Clone, Copy)]
pub enum AdvancedQueryCapacity {
Cte {
cte_count: usize,
avg_query_len: usize,
},
WindowFunction {
window_count: usize,
partition_cols: usize,
order_cols: usize,
},
FullTextSearch {
columns: usize,
query_len: usize,
},
JsonPath {
depth: usize,
},
Upsert {
columns: usize,
conflict_cols: usize,
update_cols: usize,
},
ProcedureCall {
params: usize,
},
TriggerDef {
events: usize,
body_len: usize,
},
RlsPolicy {
expr_len: usize,
},
}
impl AdvancedQueryCapacity {
#[inline]
pub const fn estimate(&self) -> usize {
match self {
Self::Cte {
cte_count,
avg_query_len,
} => {
16 + *cte_count * (32 + *avg_query_len)
}
Self::WindowFunction {
window_count,
partition_cols,
order_cols,
} => {
*window_count * (48 + *partition_cols * 16 + *order_cols * 20)
}
Self::FullTextSearch { columns, query_len } => {
64 + *columns * 20 + *query_len
}
Self::JsonPath { depth } => {
16 + *depth * 12
}
Self::Upsert {
columns,
conflict_cols,
update_cols,
} => {
64 + *columns * 8 + *conflict_cols * 12 + *update_cols * 16
}
Self::ProcedureCall { params } => {
32 + *params * 8
}
Self::TriggerDef { events, body_len } => {
96 + *events * 12 + *body_len
}
Self::RlsPolicy { expr_len } => {
64 + *expr_len
}
}
}
#[inline]
pub const fn to_query_capacity(&self) -> QueryCapacity {
QueryCapacity::Custom(self.estimate())
}
}
impl FastSqlBuilder {
#[inline]
pub fn for_advanced(db_type: DatabaseType, capacity: AdvancedQueryCapacity) -> Self {
Self::with_capacity(db_type, capacity.to_query_capacity())
}
#[inline]
pub fn for_cte(db_type: DatabaseType, cte_count: usize, avg_query_len: usize) -> Self {
Self::for_advanced(
db_type,
AdvancedQueryCapacity::Cte {
cte_count,
avg_query_len,
},
)
}
#[inline]
pub fn for_window(
db_type: DatabaseType,
window_count: usize,
partition_cols: usize,
order_cols: usize,
) -> Self {
Self::for_advanced(
db_type,
AdvancedQueryCapacity::WindowFunction {
window_count,
partition_cols,
order_cols,
},
)
}
#[inline]
pub fn for_upsert(
db_type: DatabaseType,
columns: usize,
conflict_cols: usize,
update_cols: usize,
) -> Self {
Self::for_advanced(
db_type,
AdvancedQueryCapacity::Upsert {
columns,
conflict_cols,
update_cols,
},
)
}
}
pub mod parse {
pub fn extract_where_body(sql: &str) -> Option<String> {
let lower = sql.to_ascii_lowercase();
let i = lower.find(" where ")?;
Some(sql[i + " where ".len()..].trim().to_string())
}
pub fn extract_insert_columns(sql: &str) -> Option<Vec<String>> {
let open = sql.find('(')?;
let close = sql[open..].find(')').map(|i| open + i)?;
let body = &sql[open + 1..close];
Some(
body.split(',')
.map(|c| c.trim().to_string())
.filter(|c| !c.is_empty())
.collect(),
)
}
pub fn count_set_placeholders(sql: &str) -> Option<usize> {
let lower = sql.to_ascii_lowercase();
let set_start = lower.find(" set ")?;
let where_start = lower[set_start..]
.find(" where ")
.map(|i| set_start + i)
.unwrap_or(sql.len());
Some(sql[set_start..where_start].matches('?').count())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_where_body_finds_lowercase_tail() {
assert_eq!(
extract_where_body("UPDATE t SET a = 1 WHERE id = 42"),
Some("id = 42".to_string())
);
}
#[test]
fn extract_where_body_case_insensitive() {
assert_eq!(
extract_where_body("update t set a = 1 where id = 42"),
Some("id = 42".to_string())
);
}
#[test]
fn extract_where_body_missing_returns_none() {
assert_eq!(extract_where_body("SELECT * FROM t"), None);
}
#[test]
fn extract_insert_columns_parses_list() {
assert_eq!(
extract_insert_columns("INSERT INTO users (id, email, name) VALUES ($1, $2, $3)"),
Some(vec!["id".into(), "email".into(), "name".into()])
);
}
#[test]
fn count_set_placeholders_counts_between_set_and_where() {
assert_eq!(
count_set_placeholders("UPDATE t SET a = ?, b = ? WHERE id = ?"),
Some(2)
);
}
#[test]
fn count_set_placeholders_without_where_counts_to_end() {
assert_eq!(count_set_placeholders("UPDATE t SET a = ?, b = ?"), Some(2));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_identifier() {
assert_eq!(escape_identifier("user"), "\"user\"");
assert_eq!(escape_identifier("my_table"), "\"my_table\"");
assert_eq!(escape_identifier("has\"quote"), "\"has\"\"quote\"");
}
#[test]
fn test_needs_quoting() {
assert!(needs_quoting("user"));
assert!(needs_quoting("order"));
assert!(needs_quoting("has space"));
assert!(!needs_quoting("my_table"));
assert!(!needs_quoting("users"));
}
#[test]
fn test_quote_identifier() {
assert_eq!(quote_identifier("user"), "\"user\"");
assert_eq!(quote_identifier("my_table"), "my_table");
}
#[test]
fn test_database_placeholder() {
assert_eq!(DatabaseType::PostgreSQL.placeholder(1).as_ref(), "$1");
assert_eq!(DatabaseType::PostgreSQL.placeholder(5).as_ref(), "$5");
assert_eq!(DatabaseType::PostgreSQL.placeholder(100).as_ref(), "$100");
assert_eq!(DatabaseType::PostgreSQL.placeholder(128).as_ref(), "$128");
assert_eq!(DatabaseType::PostgreSQL.placeholder(256).as_ref(), "$256");
assert_eq!(DatabaseType::MySQL.placeholder(1).as_ref(), "?");
assert_eq!(DatabaseType::SQLite.placeholder(1).as_ref(), "?");
assert!(matches!(
DatabaseType::MySQL.placeholder(1),
Cow::Borrowed(_)
));
assert!(matches!(
DatabaseType::SQLite.placeholder(1),
Cow::Borrowed(_)
));
assert!(matches!(
DatabaseType::PostgreSQL.placeholder(1),
Cow::Borrowed(_)
));
assert!(matches!(
DatabaseType::PostgreSQL.placeholder(50),
Cow::Borrowed(_)
));
assert!(matches!(
DatabaseType::PostgreSQL.placeholder(128),
Cow::Borrowed(_)
));
assert!(matches!(
DatabaseType::PostgreSQL.placeholder(256),
Cow::Borrowed(_)
));
assert!(matches!(
DatabaseType::PostgreSQL.placeholder(257),
Cow::Owned(_)
));
assert_eq!(DatabaseType::PostgreSQL.placeholder(257).as_ref(), "$257");
assert_eq!(DatabaseType::PostgreSQL.placeholder(200).as_ref(), "$200");
assert!(matches!(
DatabaseType::PostgreSQL.placeholder(0),
Cow::Owned(_)
));
assert_eq!(DatabaseType::PostgreSQL.placeholder(0).as_ref(), "$0");
}
#[test]
fn test_sql_builder() {
let mut builder = SqlBuilder::postgres();
builder
.push("SELECT * FROM ")
.push_identifier("user")
.push(" WHERE ")
.push_identifier("id")
.push(" = ")
.push_param(42i32);
let (sql, params) = builder.build();
assert_eq!(sql, "SELECT * FROM \"user\" WHERE id = $1");
assert_eq!(params.len(), 1);
}
#[test]
fn test_fast_builder_simple() {
let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
builder.push_str("SELECT * FROM users WHERE id = ");
builder.bind(42i64);
let (sql, params) = builder.build();
assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
assert_eq!(params.len(), 1);
}
#[test]
fn test_fast_builder_complex() {
let mut builder = FastSqlBuilder::with_capacity(
DatabaseType::PostgreSQL,
QueryCapacity::SelectWithFilters(5),
);
builder
.push_str("SELECT * FROM users WHERE active = ")
.bind(true)
.push_str(" AND age > ")
.bind(18i64)
.push_str(" AND status = ")
.bind("approved")
.push_str(" ORDER BY created_at LIMIT ")
.bind(10i64);
let (sql, params) = builder.build();
assert!(sql.contains("$1"));
assert!(sql.contains("$4"));
assert_eq!(params.len(), 4);
}
#[test]
fn test_fast_builder_in_clause_postgres() {
let mut builder = FastSqlBuilder::postgres(QueryCapacity::Custom(128));
builder.push_str("SELECT * FROM users WHERE id IN (");
let values: Vec<FilterValue> = (1..=5).map(FilterValue::Int).collect();
builder.bind_in_clause(values);
builder.push_char(')');
let (sql, params) = builder.build();
assert_eq!(sql, "SELECT * FROM users WHERE id IN ($1, $2, $3, $4, $5)");
assert_eq!(params.len(), 5);
}
#[test]
fn test_fast_builder_in_clause_mysql() {
let mut builder = FastSqlBuilder::mysql(QueryCapacity::Custom(128));
builder.push_str("SELECT * FROM users WHERE id IN (");
let values: Vec<FilterValue> = (1..=5).map(FilterValue::Int).collect();
builder.bind_in_clause(values);
builder.push_char(')');
let (sql, params) = builder.build();
assert_eq!(sql, "SELECT * FROM users WHERE id IN (?, ?, ?, ?, ?)");
assert_eq!(params.len(), 5);
}
#[test]
fn test_fast_builder_identifier() {
let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
builder.push_str("SELECT * FROM ");
builder.push_identifier("user"); builder.push_str(" WHERE ");
builder.push_identifier("my_column"); builder.push_str(" = ");
builder.bind(1i64);
let (sql, _) = builder.build();
assert_eq!(sql, "SELECT * FROM \"user\" WHERE my_column = $1");
}
#[test]
fn test_fast_builder_identifier_with_quotes() {
let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
builder.push_str("SELECT * FROM ");
builder.push_identifier("has\"quote");
let sql = builder.build_sql();
assert_eq!(sql, "SELECT * FROM \"has\"\"quote\"");
}
#[test]
fn test_fast_builder_conditional() {
let mut builder = FastSqlBuilder::postgres(QueryCapacity::SelectWithFilters(2));
builder.push_str("SELECT * FROM users WHERE 1=1");
builder.push_if(true, " AND active = true");
builder.push_if(false, " AND deleted = false");
let sql = builder.build_sql();
assert_eq!(sql, "SELECT * FROM users WHERE 1=1 AND active = true");
}
#[test]
fn test_template_select_by_id() {
let sql = templates::select_by_id("users", &["id", "name", "email"]);
assert_eq!(sql, "SELECT id, name, email FROM users WHERE id = $1");
}
#[test]
fn test_template_select_by_id_all_columns() {
let sql = templates::select_by_id("users", &[]);
assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
}
#[test]
fn test_template_insert_returning() {
let sql = templates::insert_returning("users", &["name", "email"]);
assert_eq!(
sql,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *"
);
}
#[test]
fn test_template_update_by_id() {
let sql = templates::update_by_id("users", &["name", "email"]);
assert_eq!(sql, "UPDATE users SET name = $1, email = $2 WHERE id = $3");
}
#[test]
fn test_template_delete_by_id() {
let sql = templates::delete_by_id("users");
assert_eq!(sql, "DELETE FROM users WHERE id = $1");
}
#[test]
fn test_template_batch_placeholders_postgres() {
let sql = templates::batch_placeholders(DatabaseType::PostgreSQL, 3, 2);
assert_eq!(sql, "($1, $2, $3), ($4, $5, $6)");
}
#[test]
fn test_template_batch_placeholders_mysql() {
let sql = templates::batch_placeholders(DatabaseType::MySQL, 3, 2);
assert_eq!(sql, "(?, ?, ?), (?, ?, ?)");
}
#[test]
fn test_query_capacity_estimates() {
assert_eq!(QueryCapacity::SimpleSelect.estimate(), 64);
assert_eq!(QueryCapacity::SelectWithFilters(5).estimate(), 64 + 5 * 32);
assert_eq!(QueryCapacity::Insert(10).estimate(), 32 + 10 * 16);
assert_eq!(QueryCapacity::Update(5).estimate(), 32 + 5 * 20);
assert_eq!(QueryCapacity::Delete.estimate(), 48);
assert_eq!(QueryCapacity::Custom(256).estimate(), 256);
}
}