use anyhow::{bail, Result};
use core::fmt::Debug;
use chrono::format::{Fixed, Item, Numeric, Pad, StrftimeItems};
use serde::{Deserialize, Serialize};
use std::any::{Any, TypeId};
use strum::VariantNames;
#[derive(
Debug,
PartialEq,
Eq,
Clone,
Copy,
Serialize,
Default,
Deserialize,
strum::Display,
strum::EnumIter,
strum::EnumMessage,
strum::EnumString,
strum::EnumVariantNames,
)]
#[strum(serialize_all = "lowercase")]
pub enum Dialect {
Ansi,
BigQuery,
ClickHouse,
DuckDb,
#[default]
Generic,
GlareDb,
MsSql,
MySql,
Postgres,
SQLite,
Snowflake,
}
impl Dialect {
pub(super) fn handler(&self) -> Box<dyn DialectHandler> {
match self {
Dialect::MsSql => Box::new(MsSqlDialect),
Dialect::MySql => Box::new(MySqlDialect),
Dialect::BigQuery => Box::new(BigQueryDialect),
Dialect::SQLite => Box::new(SQLiteDialect),
Dialect::ClickHouse => Box::new(ClickHouseDialect),
Dialect::Snowflake => Box::new(SnowflakeDialect),
Dialect::DuckDb => Box::new(DuckDbDialect),
Dialect::Postgres => Box::new(PostgresDialect),
Dialect::GlareDb => Box::new(GlareDbDialect),
Dialect::Ansi | Dialect::Generic => Box::new(GenericDialect),
}
}
pub fn support_level(&self) -> SupportLevel {
match self {
Dialect::DuckDb
| Dialect::SQLite
| Dialect::Postgres
| Dialect::MySql
| Dialect::Generic
| Dialect::GlareDb
| Dialect::ClickHouse => SupportLevel::Supported,
Dialect::MsSql | Dialect::Ansi | Dialect::BigQuery | Dialect::Snowflake => {
SupportLevel::Unsupported
}
}
}
#[deprecated(note = "Use `Dialect::VARIANTS` instead")]
pub fn names() -> &'static [&'static str] {
Dialect::VARIANTS
}
}
pub enum SupportLevel {
Supported,
Unsupported,
Nascent,
}
#[derive(Debug)]
pub struct GenericDialect;
#[derive(Debug)]
pub struct SQLiteDialect;
#[derive(Debug)]
pub struct MySqlDialect;
#[derive(Debug)]
pub struct MsSqlDialect;
#[derive(Debug)]
pub struct BigQueryDialect;
#[derive(Debug)]
pub struct ClickHouseDialect;
#[derive(Debug)]
pub struct SnowflakeDialect;
#[derive(Debug)]
pub struct DuckDbDialect;
#[derive(Debug)]
pub struct PostgresDialect;
#[derive(Debug)]
pub struct GlareDbDialect;
pub(super) enum ColumnExclude {
Exclude,
Except,
}
pub(super) trait DialectHandler: Any + Debug {
fn use_fetch(&self) -> bool {
false
}
fn ident_quote(&self) -> char {
'"'
}
fn column_exclude(&self) -> Option<ColumnExclude> {
None
}
fn set_ops_distinct(&self) -> bool {
true
}
fn except_all(&self) -> bool {
true
}
fn intersect_all(&self) -> bool {
self.except_all()
}
fn has_concat_function(&self) -> bool {
true
}
fn requires_quotes_intervals(&self) -> bool {
false
}
fn stars_in_group(&self) -> bool {
true
}
fn supports_distinct_on(&self) -> bool {
false
}
fn translate_prql_date_format(&self, prql_date_format: &str) -> Result<String> {
Ok(StrftimeItems::new(prql_date_format)
.map(|item| self.translate_chrono_item(item))
.collect::<Result<Vec<_>>>()?
.join(""))
}
fn translate_chrono_item(&self, _item: Item) -> Result<String> {
bail!("Date formatting is not yet supported for this dialect")
}
}
impl dyn DialectHandler {
#[inline]
pub fn is<T: DialectHandler + 'static>(&self) -> bool {
TypeId::of::<T>() == self.type_id()
}
}
impl DialectHandler for GenericDialect {
fn translate_chrono_item(&self, _item: Item) -> Result<String> {
bail!("Date formatting requires a dialect")
}
}
impl DialectHandler for PostgresDialect {
fn requires_quotes_intervals(&self) -> bool {
true
}
fn supports_distinct_on(&self) -> bool {
true
}
fn translate_chrono_item<'a>(&self, item: Item) -> Result<String> {
Ok(match item {
Item::Numeric(Numeric::Year, Pad::Zero) => "YYYY".to_string(),
Item::Numeric(Numeric::YearMod100, Pad::Zero) => "YY".to_string(),
Item::Numeric(Numeric::Month, Pad::None) => "FMMM".to_string(),
Item::Numeric(Numeric::Month, Pad::Zero) => "MM".to_string(),
Item::Numeric(Numeric::Day, Pad::None) => "FMDD".to_string(),
Item::Numeric(Numeric::Day, Pad::Zero) => "DD".to_string(),
Item::Numeric(Numeric::Hour, Pad::None) => "FMHH24".to_string(),
Item::Numeric(Numeric::Hour, Pad::Zero) => "HH24".to_string(),
Item::Numeric(Numeric::Hour12, Pad::Zero) => "HH12".to_string(),
Item::Numeric(Numeric::Minute, Pad::Zero) => "MI".to_string(),
Item::Numeric(Numeric::Second, Pad::Zero) => "SS".to_string(),
Item::Numeric(Numeric::Nanosecond, Pad::Zero) => "US".to_string(), Item::Fixed(Fixed::ShortMonthName) => "Mon".to_string(),
Item::Fixed(Fixed::LongMonthName) => "FMMonth".to_string(),
Item::Fixed(Fixed::ShortWeekdayName) => "Dy".to_string(),
Item::Fixed(Fixed::LongWeekdayName) => "FMDay".to_string(),
Item::Fixed(Fixed::UpperAmPm) => "AM".to_string(),
Item::Fixed(Fixed::RFC3339) => "YYYY-MM-DD\"T\"HH24:MI:SS.USZ".to_string(),
Item::Literal(literal) => {
if literal.chars().any(|c| c.is_ascii_alphanumeric()) {
format!("\"{}\"", literal)
} else {
literal.replace('\'', "''").replace('"', "\\\"")
}
}
Item::Space(spaces) => spaces.to_string(),
_ => bail!("PRQL doesn't support this format specifier"),
})
}
}
impl DialectHandler for GlareDbDialect {
fn requires_quotes_intervals(&self) -> bool {
true
}
}
impl DialectHandler for SQLiteDialect {
fn set_ops_distinct(&self) -> bool {
false
}
fn except_all(&self) -> bool {
false
}
fn has_concat_function(&self) -> bool {
false
}
fn stars_in_group(&self) -> bool {
false
}
}
impl DialectHandler for MsSqlDialect {
fn use_fetch(&self) -> bool {
true
}
fn except_all(&self) -> bool {
false
}
fn set_ops_distinct(&self) -> bool {
false
}
fn translate_chrono_item<'a>(&self, item: Item) -> Result<String> {
Ok(match item {
Item::Numeric(Numeric::Year, Pad::Zero) => "yyyy".to_string(),
Item::Numeric(Numeric::YearMod100, Pad::Zero) => "yy".to_string(),
Item::Numeric(Numeric::Month, Pad::None) => "M".to_string(),
Item::Numeric(Numeric::Month, Pad::Zero) => "MM".to_string(),
Item::Numeric(Numeric::Day, Pad::None) => "d".to_string(),
Item::Numeric(Numeric::Day, Pad::Zero) => "dd".to_string(),
Item::Numeric(Numeric::Hour, Pad::None) => "H".to_string(),
Item::Numeric(Numeric::Hour, Pad::Zero) => "HH".to_string(),
Item::Numeric(Numeric::Hour12, Pad::Zero) => "hh".to_string(),
Item::Numeric(Numeric::Minute, Pad::Zero) => "mm".to_string(),
Item::Numeric(Numeric::Second, Pad::Zero) => "ss".to_string(),
Item::Numeric(Numeric::Nanosecond, Pad::Zero) => "ffffff".to_string(), Item::Fixed(Fixed::ShortMonthName) => "MMM".to_string(),
Item::Fixed(Fixed::LongMonthName) => "MMMM".to_string(),
Item::Fixed(Fixed::ShortWeekdayName) => "ddd".to_string(),
Item::Fixed(Fixed::LongWeekdayName) => "dddd".to_string(),
Item::Fixed(Fixed::UpperAmPm) => "tt".to_string(),
Item::Fixed(Fixed::RFC3339) => "yyyy-MM-dd'T'HH:mm:ss.ffffff'Z'".to_string(),
Item::Literal(literal) => {
if literal.chars().any(|c| c.is_ascii_alphanumeric()) {
format!("\"{}\"", literal)
} else {
literal
.replace('"', "\\\"")
.replace('\'', "\"\'\"")
.replace('%', "\\%")
}
}
Item::Space(spaces) => spaces.to_string(),
_ => bail!("PRQL doesn't support this format specifier"),
})
}
}
impl DialectHandler for MySqlDialect {
fn ident_quote(&self) -> char {
'`'
}
fn set_ops_distinct(&self) -> bool {
true
}
fn translate_chrono_item<'a>(&self, item: Item) -> Result<String> {
Ok(match item {
Item::Numeric(Numeric::Year, Pad::Zero) => "%Y".to_string(),
Item::Numeric(Numeric::YearMod100, Pad::Zero) => "%y".to_string(),
Item::Numeric(Numeric::Month, Pad::None) => "%c".to_string(),
Item::Numeric(Numeric::Month, Pad::Zero) => "%m".to_string(),
Item::Numeric(Numeric::Day, Pad::None) => "%e".to_string(),
Item::Numeric(Numeric::Day, Pad::Zero) => "%d".to_string(),
Item::Numeric(Numeric::Hour, Pad::None) => "%k".to_string(),
Item::Numeric(Numeric::Hour, Pad::Zero) => "%H".to_string(),
Item::Numeric(Numeric::Hour12, Pad::Zero) => "%I".to_string(),
Item::Numeric(Numeric::Minute, Pad::Zero) => "%i".to_string(),
Item::Numeric(Numeric::Second, Pad::Zero) => "%S".to_string(),
Item::Numeric(Numeric::Nanosecond, Pad::Zero) => "%f".to_string(), Item::Fixed(Fixed::ShortMonthName) => "%b".to_string(),
Item::Fixed(Fixed::LongMonthName) => "%M".to_string(),
Item::Fixed(Fixed::ShortWeekdayName) => "%a".to_string(),
Item::Fixed(Fixed::LongWeekdayName) => "%W".to_string(),
Item::Fixed(Fixed::UpperAmPm) => "%p".to_string(),
Item::Fixed(Fixed::RFC3339) => "%Y-%m-%dT%H:%i:%S.%fZ".to_string(),
Item::Literal(literal) => literal.replace('\'', "''").replace('%', "%%"),
Item::Space(spaces) => spaces.to_string(),
_ => bail!("PRQL doesn't support this format specifier"),
})
}
}
impl DialectHandler for ClickHouseDialect {
fn ident_quote(&self) -> char {
'`'
}
fn supports_distinct_on(&self) -> bool {
true
}
fn translate_chrono_item<'a>(&self, item: Item) -> Result<String> {
Ok(match item {
Item::Numeric(Numeric::Year, Pad::Zero) => "yyyy".to_string(),
Item::Numeric(Numeric::YearMod100, Pad::Zero) => "yy".to_string(),
Item::Numeric(Numeric::Month, Pad::None) => "M".to_string(),
Item::Numeric(Numeric::Month, Pad::Zero) => "MM".to_string(),
Item::Numeric(Numeric::Day, Pad::None) => "d".to_string(),
Item::Numeric(Numeric::Day, Pad::Zero) => "dd".to_string(),
Item::Numeric(Numeric::Hour, Pad::None) => "H".to_string(),
Item::Numeric(Numeric::Hour, Pad::Zero) => "HH".to_string(),
Item::Numeric(Numeric::Hour12, Pad::Zero) => "hh".to_string(),
Item::Numeric(Numeric::Minute, Pad::Zero) => "mm".to_string(),
Item::Numeric(Numeric::Second, Pad::Zero) => "ss".to_string(),
Item::Numeric(Numeric::Nanosecond, Pad::Zero) => "SSSSSS".to_string(), Item::Fixed(Fixed::ShortMonthName) => "MMM".to_string(),
Item::Fixed(Fixed::LongMonthName) => "MMMM".to_string(),
Item::Fixed(Fixed::ShortWeekdayName) => "EEE".to_string(),
Item::Fixed(Fixed::LongWeekdayName) => "EEEE".to_string(),
Item::Fixed(Fixed::UpperAmPm) => "aa".to_string(),
Item::Fixed(Fixed::RFC3339) => "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'".to_string(),
Item::Literal(literal) => {
if literal.chars().any(|c| c.is_ascii_alphanumeric()) {
format!("'{}'", literal)
} else {
literal.replace('\'', "\\'\\'")
}
}
Item::Space(spaces) => spaces.to_string(),
_ => bail!("PRQL doesn't support this format specifier"),
})
}
}
impl DialectHandler for BigQueryDialect {
fn ident_quote(&self) -> char {
'`'
}
fn column_exclude(&self) -> Option<ColumnExclude> {
Some(ColumnExclude::Except)
}
fn set_ops_distinct(&self) -> bool {
true
}
}
impl DialectHandler for SnowflakeDialect {
fn column_exclude(&self) -> Option<ColumnExclude> {
Some(ColumnExclude::Exclude)
}
fn set_ops_distinct(&self) -> bool {
false
}
}
impl DialectHandler for DuckDbDialect {
fn column_exclude(&self) -> Option<ColumnExclude> {
Some(ColumnExclude::Exclude)
}
fn except_all(&self) -> bool {
false
}
fn supports_distinct_on(&self) -> bool {
true
}
fn translate_chrono_item<'a>(&self, item: Item) -> Result<String> {
Ok(match item {
Item::Numeric(Numeric::Year, Pad::Zero) => "%Y".to_string(),
Item::Numeric(Numeric::YearMod100, Pad::Zero) => "%y".to_string(),
Item::Numeric(Numeric::Month, Pad::None) => "%-m".to_string(),
Item::Numeric(Numeric::Month, Pad::Zero) => "%m".to_string(),
Item::Numeric(Numeric::Day, Pad::None) => "%-d".to_string(),
Item::Numeric(Numeric::Day, Pad::Zero) => "%d".to_string(),
Item::Numeric(Numeric::Hour, Pad::None) => "%-H".to_string(),
Item::Numeric(Numeric::Hour, Pad::Zero) => "%H".to_string(),
Item::Numeric(Numeric::Hour12, Pad::Zero) => "%I".to_string(),
Item::Numeric(Numeric::Minute, Pad::Zero) => "%M".to_string(),
Item::Numeric(Numeric::Second, Pad::Zero) => "%S".to_string(),
Item::Numeric(Numeric::Nanosecond, Pad::Zero) => "%f".to_string(), Item::Fixed(Fixed::ShortMonthName) => "%b".to_string(),
Item::Fixed(Fixed::LongMonthName) => "%B".to_string(),
Item::Fixed(Fixed::ShortWeekdayName) => "%a".to_string(),
Item::Fixed(Fixed::LongWeekdayName) => "%A".to_string(),
Item::Fixed(Fixed::UpperAmPm) => "%p".to_string(),
Item::Fixed(Fixed::RFC3339) => "%Y-%m-%dT%H:%M:%S.%fZ".to_string(),
Item::Literal(literal) => literal.replace('\'', "''").replace('%', "%%"),
Item::Space(spaces) => spaces.to_string(),
_ => bail!("PRQL doesn't support this format specifier"),
})
}
}
#[cfg(test)]
mod tests {
use super::Dialect;
use insta::assert_debug_snapshot;
use std::str::FromStr;
#[test]
fn test_dialect_from_str() {
assert_debug_snapshot!(Dialect::from_str("postgres"), @r###"
Ok(
Postgres,
)
"###);
assert_debug_snapshot!(Dialect::from_str("foo"), @r###"
Err(
VariantNotFound,
)
"###);
}
}