1use chrono::{NaiveDate, NaiveDateTime};
2
3use super::Dialect;
4use crate::{Driver, Error, Value};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub(crate) enum ColumnType {
8 Id,
9 String(u32),
10 Text,
11 Integer,
12 BigInteger,
13 Boolean,
14 DateTime,
15}
16
17#[derive(Debug, Clone)]
19pub struct Column {
20 pub(crate) name: String,
21 pub(crate) ty: ColumnType,
22 nullable: bool,
23 default: Option<Value>,
24}
25
26impl Column {
27 pub(crate) fn new(name: &str, ty: ColumnType) -> Self {
28 Self {
29 name: name.to_owned(),
30 ty,
31 nullable: false,
32 default: None,
33 }
34 }
35
36 pub fn nullable(&mut self) -> &mut Self {
38 self.nullable = true;
39 self
40 }
41
42 pub fn default(&mut self, value: impl Into<Value>) -> &mut Self {
44 self.default = Some(value.into());
45 self
46 }
47
48 pub fn length(&mut self, length: u32) -> &mut Self {
50 if let ColumnType::String(n) = &mut self.ty {
51 *n = length;
52 }
53 self
54 }
55
56 pub(crate) fn render(&self, driver: Driver, dialect: Dialect) -> Result<String, Error> {
58 crate::query::validate(&self.name)?;
59 let name = driver.quote(&self.name);
60 if self.ty == ColumnType::Id {
61 let id = match dialect {
62 Dialect::MySql => "BIGINT AUTO_INCREMENT PRIMARY KEY",
63 Dialect::Postgres => "BIGSERIAL PRIMARY KEY",
65 Dialect::Sqlite => "INTEGER PRIMARY KEY AUTOINCREMENT",
66 };
67 return Ok(format!("{name} {id}"));
68 }
69 let mut sql = format!("{name} {}", self.sql_type(dialect));
70 sql.push_str(if self.nullable { " NULL" } else { " NOT NULL" });
71 if let Some(value) = &self.default {
72 sql.push_str(" DEFAULT ");
73 sql.push_str(&literal(value, driver, dialect)?);
74 }
75 Ok(sql)
76 }
77
78 fn sql_type(&self, dialect: Dialect) -> String {
79 match (self.ty, dialect) {
80 (ColumnType::Id, _) => unreachable!("an id column is rendered whole"),
81 (ColumnType::String(_), Dialect::Sqlite) => "TEXT".to_owned(),
82 (ColumnType::String(n), _) => format!("VARCHAR({n})"),
83 (ColumnType::Text, _) => "TEXT".to_owned(),
84 (ColumnType::Integer, _) => "INTEGER".to_owned(),
85 (ColumnType::BigInteger, Dialect::Sqlite) => "INTEGER".to_owned(),
86 (ColumnType::BigInteger, _) => "BIGINT".to_owned(),
87 (ColumnType::Boolean, Dialect::Postgres) => "BOOLEAN".to_owned(),
88 (ColumnType::Boolean, _) => "TINYINT(1)".to_owned(),
89 (ColumnType::DateTime, Dialect::Postgres) => {
90 "TIMESTAMP(0) WITHOUT TIME ZONE".to_owned()
91 }
92 (ColumnType::DateTime, _) => "DATETIME".to_owned(),
93 }
94 }
95}
96
97fn literal(value: &Value, driver: Driver, dialect: Dialect) -> Result<String, Error> {
99 let unsupported = || Error::Unsupported {
100 operation: "a column default of this type",
101 driver: driver.name(),
102 };
103 Ok(match value {
104 Value::Null => "NULL".to_owned(),
105 Value::Bool(b) => match (dialect, b) {
106 (Dialect::Postgres, true) => "true".to_owned(),
107 (Dialect::Postgres, false) => "false".to_owned(),
108 (_, true) => "1".to_owned(),
109 (_, false) => "0".to_owned(),
110 },
111 Value::Int(n) => n.to_string(),
112 Value::UInt(n) => n.to_string(),
113 Value::Float(f) if f.is_finite() => format!("{f:?}"),
114 Value::Float(_) => return Err(unsupported()),
115 Value::Text(s) => quote_text(s, dialect),
116 Value::DateTime(d) => quote_text(&format_datetime(d), dialect),
117 Value::Date(d) => quote_text(&format_date(d), dialect),
118 Value::Bytes(_) => return Err(unsupported()),
119 })
120}
121
122fn format_datetime(d: &NaiveDateTime) -> String {
123 d.format("%Y-%m-%d %H:%M:%S").to_string()
124}
125
126fn format_date(d: &NaiveDate) -> String {
127 d.format("%Y-%m-%d").to_string()
128}
129
130fn quote_text(s: &str, dialect: Dialect) -> String {
133 let mut out = s.replace('\'', "''");
134 if dialect == Dialect::MySql {
135 out = out.replace('\\', "\\\\");
136 }
137 format!("'{out}'")
138}