octofhir_sof/sql_generator/
ddl.rs1use std::fmt;
4use std::str::FromStr;
5
6use crate::column::ColumnType;
7
8use super::GeneratedColumn;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum Dialect {
13 #[default]
15 Ansi,
16 Postgres,
18 DuckDb,
20}
21
22impl FromStr for Dialect {
23 type Err = String;
24
25 fn from_str(s: &str) -> Result<Self, Self::Err> {
26 match s.trim().to_lowercase().as_str() {
27 "ansi" | "sql" => Ok(Self::Ansi),
28 "postgres" | "postgresql" | "pg" => Ok(Self::Postgres),
29 "duckdb" | "duck" => Ok(Self::DuckDb),
30 other => Err(format!(
31 "unknown dialect `{other}` (expected ansi, postgres or duckdb)"
32 )),
33 }
34 }
35}
36
37impl fmt::Display for Dialect {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 f.write_str(match self {
40 Self::Ansi => "ansi",
41 Self::Postgres => "postgres",
42 Self::DuckDb => "duckdb",
43 })
44 }
45}
46
47impl Dialect {
48 fn type_name(&self, ty: ColumnType) -> &'static str {
50 match self {
51 Self::Ansi => ty.ansi_type(),
52 Self::Postgres => ty.sql_type(),
53 Self::DuckDb => match ty {
54 ColumnType::String => "VARCHAR",
55 ColumnType::Integer => "INTEGER",
56 ColumnType::Integer64 => "BIGINT",
57 ColumnType::Decimal => "DOUBLE",
58 ColumnType::Boolean => "BOOLEAN",
59 ColumnType::Date => "DATE",
60 ColumnType::DateTime | ColumnType::Instant => "TIMESTAMP",
61 ColumnType::Time => "TIME",
62 ColumnType::Base64Binary => "BLOB",
63 ColumnType::Json => "JSON",
64 },
65 }
66 }
67}
68
69pub fn create_table(table: &str, columns: &[GeneratedColumn], dialect: Dialect) -> String {
71 let ident = quote_ident(table);
72 if columns.is_empty() {
73 return format!("CREATE TABLE {ident} ();\n");
74 }
75 let mut out = format!("CREATE TABLE {ident} (\n");
76 for (i, col) in columns.iter().enumerate() {
77 let comma = if i + 1 < columns.len() { "," } else { "" };
78 out.push_str(&format!(
79 " {} {}{}\n",
80 quote_ident(&col.name),
81 dialect.type_name(col.col_type),
82 comma
83 ));
84 }
85 out.push_str(");\n");
86 out
87}
88
89fn quote_ident(name: &str) -> String {
91 format!("\"{}\"", name.replace('"', "\"\""))
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 fn cols() -> Vec<GeneratedColumn> {
99 vec![
100 GeneratedColumn {
101 name: "id".into(),
102 expression: String::new(),
103 alias: "id".into(),
104 col_type: ColumnType::String,
105 },
106 GeneratedColumn {
107 name: "age".into(),
108 expression: String::new(),
109 alias: "age".into(),
110 col_type: ColumnType::Integer,
111 },
112 GeneratedColumn {
113 name: "born".into(),
114 expression: String::new(),
115 alias: "born".into(),
116 col_type: ColumnType::Date,
117 },
118 ]
119 }
120
121 #[test]
122 fn ansi_ddl_uses_character_varying() {
123 let sql = create_table("patient_view", &cols(), Dialect::Ansi);
124 assert!(sql.contains("CREATE TABLE \"patient_view\" ("));
125 assert!(sql.contains("\"id\" CHARACTER VARYING,"));
126 assert!(sql.contains("\"age\" INT,"));
127 assert!(sql.contains("\"born\" CHARACTER VARYING\n"));
128 assert!(sql.trim_end().ends_with(");"));
129 }
130
131 #[test]
132 fn postgres_ddl_uses_native_types() {
133 let sql = create_table("v", &cols(), Dialect::Postgres);
134 assert!(sql.contains("\"id\" TEXT,"));
135 assert!(sql.contains("\"age\" INTEGER,"));
136 assert!(sql.contains("\"born\" DATE\n"));
137 }
138
139 #[test]
140 fn dialect_parses() {
141 assert_eq!("pg".parse::<Dialect>().unwrap(), Dialect::Postgres);
142 assert_eq!("DuckDB".parse::<Dialect>().unwrap(), Dialect::DuckDb);
143 assert!("oracle".parse::<Dialect>().is_err());
144 }
145}