use sqlparser::ast::Ident;
use sqlparser::dialect::{
AnsiDialect, BigQueryDialect, ClickHouseDialect, DatabricksDialect, Dialect, DuckDbDialect,
HiveDialect, MsSqlDialect, MySqlDialect, OracleDialect, PostgreSqlDialect, RedshiftSqlDialect,
SQLiteDialect, SnowflakeDialect,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CaseRule {
Upper,
Lower,
Insensitive,
Sensitive,
}
impl CaseRule {
pub(crate) fn normalize(self, ident: &Ident) -> String {
match self {
CaseRule::Upper if ident.quote_style.is_none() => ident.value.to_ascii_uppercase(),
CaseRule::Lower if ident.quote_style.is_none() => ident.value.to_ascii_lowercase(),
CaseRule::Upper | CaseRule::Lower | CaseRule::Sensitive => ident.value.clone(),
CaseRule::Insensitive => ident.value.to_ascii_lowercase(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IdentifierCasing {
pub table: CaseRule,
pub table_alias: CaseRule,
pub column: CaseRule,
}
impl IdentifierCasing {
pub const fn uniform(fold: CaseRule) -> Self {
Self {
table: fold,
table_alias: fold,
column: fold,
}
}
pub fn for_dialect(dialect: &dyn Dialect) -> Self {
if dialect.is::<PostgreSqlDialect>() {
Self::uniform(CaseRule::Lower)
} else if dialect.is::<AnsiDialect>()
|| dialect.is::<SnowflakeDialect>()
|| dialect.is::<OracleDialect>()
{
Self::uniform(CaseRule::Upper)
} else if dialect.is::<DuckDbDialect>()
|| dialect.is::<SQLiteDialect>()
|| dialect.is::<HiveDialect>()
|| dialect.is::<DatabricksDialect>()
|| dialect.is::<RedshiftSqlDialect>()
{
Self::uniform(CaseRule::Insensitive)
} else if dialect.is::<MsSqlDialect>() {
Self::uniform(CaseRule::Insensitive)
} else if dialect.is::<ClickHouseDialect>() {
Self::uniform(CaseRule::Sensitive)
} else if dialect.is::<MySqlDialect>() {
Self {
table: CaseRule::Sensitive,
table_alias: CaseRule::Insensitive,
column: CaseRule::Insensitive,
}
} else if dialect.is::<BigQueryDialect>() {
Self {
table: CaseRule::Sensitive,
table_alias: CaseRule::Insensitive,
column: CaseRule::Insensitive,
}
} else {
Self::uniform(CaseRule::Lower)
}
}
}
impl Default for IdentifierCasing {
fn default() -> Self {
Self::uniform(CaseRule::Lower)
}
}
pub(crate) fn canonical_quote(dialect: &dyn Dialect) -> char {
if dialect.is::<MySqlDialect>()
|| dialect.is::<BigQueryDialect>()
|| dialect.is::<HiveDialect>()
|| dialect.is::<DatabricksDialect>()
{
'`'
} else if dialect.is::<MsSqlDialect>() {
'['
} else {
'"'
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct IdentifierStyle {
pub(crate) casing: IdentifierCasing,
pub(crate) quote: char,
}
#[cfg(test)]
mod tests {
use super::*;
fn unquoted(s: &str) -> Ident {
Ident::new(s)
}
fn quoted(s: &str) -> Ident {
Ident::with_quote('"', s)
}
fn matches(fold: CaseRule, a: &Ident, b: &Ident) -> bool {
fold.normalize(a) == fold.normalize(b)
}
#[test]
fn quoting_matrix() {
use CaseRule::{Insensitive, Lower, Sensitive, Upper};
let cases: &[(Ident, Ident, [bool; 4])] = &[
(
unquoted("Users"),
unquoted("users"),
[true, true, true, false],
),
(
unquoted("Users"),
quoted("users"),
[false, true, true, false],
),
(
unquoted("Users"),
quoted("Users"),
[false, false, true, true],
),
(
quoted("Users"),
unquoted("users"),
[false, false, true, false],
),
(quoted("Users"), quoted("Users"), [true, true, true, true]),
(
quoted("Users"),
quoted("users"),
[false, false, true, false],
),
];
for (binding, reference, [up, lo, ci, cs]) in cases {
assert_eq!(
matches(Upper, binding, reference),
*up,
"Upper: {binding:?} vs {reference:?}"
);
assert_eq!(
matches(Lower, binding, reference),
*lo,
"Lower: {binding:?} vs {reference:?}"
);
assert_eq!(
matches(Insensitive, binding, reference),
*ci,
"Insensitive: {binding:?} vs {reference:?}"
);
assert_eq!(
matches(Sensitive, binding, reference),
*cs,
"Sensitive: {binding:?} vs {reference:?}"
);
}
}
#[test]
fn per_segment_quoting_is_independent() {
assert_eq!(CaseRule::Lower.normalize("ed("Schema")), "Schema");
assert_eq!(CaseRule::Lower.normalize(&unquoted("Table")), "table");
assert_eq!(CaseRule::Sensitive.normalize("ed("Schema")), "Schema");
assert_eq!(CaseRule::Sensitive.normalize(&unquoted("Table")), "Table");
assert_eq!(CaseRule::Insensitive.normalize("ed("Schema")), "schema");
assert_eq!(CaseRule::Insensitive.normalize(&unquoted("Table")), "table");
}
#[test]
fn dialect_casing_matrix() {
use sqlparser::dialect::GenericDialect;
use CaseRule::{Insensitive, Lower, Sensitive, Upper};
let uniform = |fold| IdentifierCasing {
table: fold,
table_alias: fold,
column: fold,
};
let split = IdentifierCasing {
table: Sensitive,
table_alias: Insensitive,
column: Insensitive,
};
let cases: Vec<(&str, Box<dyn Dialect>, IdentifierCasing)> = vec![
("PostgreSQL", Box::new(PostgreSqlDialect {}), uniform(Lower)),
(
"Redshift",
Box::new(RedshiftSqlDialect {}),
uniform(Insensitive),
),
("ANSI", Box::new(AnsiDialect {}), uniform(Upper)),
("Snowflake", Box::new(SnowflakeDialect {}), uniform(Upper)),
("Oracle", Box::new(OracleDialect {}), uniform(Upper)),
("DuckDB", Box::new(DuckDbDialect {}), uniform(Insensitive)),
("SQLite", Box::new(SQLiteDialect {}), uniform(Insensitive)),
(
"SQL Server",
Box::new(MsSqlDialect {}),
uniform(Insensitive),
),
("Hive", Box::new(HiveDialect {}), uniform(Insensitive)),
(
"Databricks",
Box::new(DatabricksDialect {}),
uniform(Insensitive),
),
(
"ClickHouse",
Box::new(ClickHouseDialect {}),
uniform(Sensitive),
),
("MySQL", Box::new(MySqlDialect {}), split),
("BigQuery", Box::new(BigQueryDialect {}), split),
("Generic", Box::new(GenericDialect {}), uniform(Lower)),
];
for (name, dialect, expected) in cases {
assert_eq!(
IdentifierCasing::for_dialect(dialect.as_ref()),
expected,
"{name}"
);
}
}
}