use super::config::Engine;
const RESERVED: &[&str] = &[
"ACCESSIBLE",
"ADD",
"ALL",
"ALTER",
"ANALYSE",
"ANALYZE",
"AND",
"ANY",
"ARRAY",
"AS",
"ASC",
"ASYMMETRIC",
"BETWEEN",
"BIGINT",
"BINARY",
"BLOB",
"BOTH",
"BY",
"CALL",
"CASCADE",
"CASE",
"CAST",
"CHANGE",
"CHAR",
"CHARACTER",
"CHECK",
"COLLATE",
"COLUMN",
"CONDITION",
"CONSTRAINT",
"CONTINUE",
"CONVERT",
"CREATE",
"CROSS",
"CUBE",
"CURRENT",
"CURRENT_CATALOG",
"CURRENT_DATE",
"CURRENT_ROLE",
"CURRENT_TIME",
"CURRENT_TIMESTAMP",
"CURRENT_USER",
"CURSOR",
"DATABASE",
"DATABASES",
"DECIMAL",
"DECLARE",
"DEFAULT",
"DEFERRABLE",
"DELAYED",
"DELETE",
"DENSE_RANK",
"DESC",
"DESCRIBE",
"DISTINCT",
"DISTINCTROW",
"DIV",
"DO",
"DOUBLE",
"DROP",
"DUAL",
"EACH",
"ELSE",
"ELSEIF",
"EMPTY",
"ENCLOSED",
"END",
"ESCAPE",
"ESCAPED",
"EXCEPT",
"EXISTS",
"EXIT",
"EXPLAIN",
"FALSE",
"FETCH",
"FIRST_VALUE",
"FLOAT",
"FOR",
"FORCE",
"FOREIGN",
"FREEZE",
"FROM",
"FULL",
"FULLTEXT",
"FUNCTION",
"GENERATED",
"GET",
"GLOB",
"GRANT",
"GROUP",
"GROUPING",
"GROUPS",
"HAVING",
"IF",
"IGNORE",
"ILIKE",
"IN",
"INDEX",
"INFILE",
"INITIALLY",
"INNER",
"INOUT",
"INSERT",
"INT",
"INTEGER",
"INTERSECT",
"INTERVAL",
"INTO",
"IS",
"ISNULL",
"ITERATE",
"JOIN",
"JSON_TABLE",
"KEY",
"KEYS",
"KILL",
"LAG",
"LAST_VALUE",
"LATERAL",
"LEAD",
"LEADING",
"LEAVE",
"LEFT",
"LIKE",
"LIMIT",
"LINEAR",
"LINES",
"LOAD",
"LOCALTIME",
"LOCALTIMESTAMP",
"LOCK",
"LONG",
"LOOP",
"MATCH",
"MOD",
"MODIFIES",
"NATURAL",
"NOT",
"NOTNULL",
"NTILE",
"NULL",
"NUMERIC",
"OF",
"OFFSET",
"ON",
"ONLY",
"OPTIMIZE",
"OPTION",
"OPTIONALLY",
"OR",
"ORDER",
"OUT",
"OUTER",
"OUTFILE",
"OVER",
"OVERLAPS",
"PARTITION",
"PLACING",
"PRECISION",
"PRIMARY",
"PROCEDURE",
"PURGE",
"RANGE",
"RANK",
"READ",
"READS",
"REAL",
"RECURSIVE",
"REFERENCES",
"REGEXP",
"RELEASE",
"RENAME",
"REPEAT",
"REPLACE",
"REQUIRE",
"RESIGNAL",
"RESTRICT",
"RETURN",
"RETURNING",
"REVOKE",
"RIGHT",
"RLIKE",
"ROW",
"ROWS",
"ROW_NUMBER",
"SCHEMA",
"SCHEMAS",
"SELECT",
"SENSITIVE",
"SEPARATOR",
"SESSION_USER",
"SET",
"SHOW",
"SIGNAL",
"SIMILAR",
"SMALLINT",
"SOME",
"SPATIAL",
"SQL",
"STARTING",
"STORED",
"SYMMETRIC",
"SYSTEM",
"SYSTEM_USER",
"TABLE",
"TABLESAMPLE",
"TERMINATED",
"THEN",
"TINYINT",
"TO",
"TRAILING",
"TRIGGER",
"TRUE",
"UNDO",
"UNION",
"UNIQUE",
"UNLOCK",
"UNSIGNED",
"UPDATE",
"USAGE",
"USE",
"USER",
"USING",
"VALUES",
"VARCHAR",
"VARIADIC",
"VERBOSE",
"VIRTUAL",
"WHEN",
"WHERE",
"WHILE",
"WINDOW",
"WITH",
"WRITE",
"XOR",
"YEAR_MONTH",
"ZEROFILL",
];
pub(crate) fn is_reserved(name: &str) -> bool {
RESERVED
.binary_search(&name.to_ascii_uppercase().as_str())
.is_ok()
}
pub fn quote_identifier(name: &str, engine: Engine) -> String {
let bare = !name.is_empty()
&& !name.starts_with(|character: char| character.is_ascii_digit())
&& name.chars().all(|character| {
character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_'
})
&& !is_reserved(name);
if bare {
return name.to_string();
}
match engine {
Engine::MySql => format!("`{}`", name.replace('`', "``")),
Engine::Postgres | Engine::Sqlite => format!("\"{}\"", name.replace('"', "\"\"")),
}
}
pub(crate) fn quote_literal(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
pub(crate) fn quote_literal_for(engine: Engine, value: &str) -> String {
let escaped = match engine {
Engine::MySql => value.replace('\\', "\\\\").replace('\'', "''"),
Engine::Postgres | Engine::Sqlite => value.replace('\'', "''"),
};
format!("'{escaped}'")
}
pub(crate) fn placeholder(engine: Engine, index: usize) -> String {
match engine {
Engine::Postgres => format!("${index}"),
Engine::MySql | Engine::Sqlite => "?".to_string(),
}
}
pub(crate) fn typed_placeholder(engine: Engine, index: usize, type_name: &str) -> String {
let placeholder = placeholder(engine, index);
match engine {
Engine::MySql if type_name.eq_ignore_ascii_case("BIT") => {
format!("cast(conv({placeholder}, 2, 10) as unsigned)")
}
Engine::Postgres if !type_name.is_empty() => {
format!("cast({placeholder} as {})", cast_target(type_name))
}
_ => placeholder,
}
}
fn cast_target(type_name: &str) -> &str {
match type_name.to_ascii_uppercase().as_str() {
"CHAR" | "BPCHAR" => "text",
"BIT" | "VARBIT" => "varbit",
_ => type_name,
}
}
pub(crate) fn text_type(engine: Engine) -> &'static str {
match engine {
Engine::MySql => "char",
Engine::Postgres | Engine::Sqlite => "text",
}
}
const KEYWORD_LITERALS: &[&str] = &["NOW()", "CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME"];
pub(crate) fn keyword_literal(value: &str) -> Option<&'static str> {
let trimmed = value.trim();
KEYWORD_LITERALS
.iter()
.find(|keyword| keyword.eq_ignore_ascii_case(trimmed))
.copied()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_reserved_list_is_sorted_for_its_search() {
assert!(RESERVED.windows(2).all(|pair| pair[0] < pair[1]));
}
#[test]
fn a_reserved_word_is_quoted_even_in_lower_case() {
assert_eq!(quote_identifier("order", Engine::Postgres), "\"order\"");
assert_eq!(quote_identifier("user", Engine::Postgres), "\"user\"");
assert_eq!(quote_identifier("key", Engine::MySql), "`key`");
assert_eq!(quote_identifier("rank", Engine::MySql), "`rank`");
assert_eq!(quote_identifier("ordered", Engine::Postgres), "ordered");
assert_eq!(quote_identifier("user_id", Engine::Sqlite), "user_id");
}
}