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 MediumText,
13 LongText,
15 Integer,
16 BigInteger,
17 Boolean,
18 DateTime(u32),
20 Timestamp(u32),
22 Binary,
23}
24
25#[derive(Debug, Clone)]
28pub struct Column {
29 pub(crate) name: String,
30 pub(crate) ty: ColumnType,
31 nullable: bool,
32 default: Option<Value>,
33 use_current: bool,
34 pub(crate) primary: bool,
35 pub(crate) change: bool,
36}
37
38impl Column {
39 pub(crate) fn new(name: &str, ty: ColumnType) -> Self {
40 Self {
41 name: name.to_owned(),
42 ty,
43 nullable: false,
44 default: None,
45 use_current: false,
46 primary: false,
47 change: false,
48 }
49 }
50
51 pub fn nullable(&mut self) -> &mut Self {
53 self.nullable = true;
54 self
55 }
56
57 pub fn default(&mut self, value: impl Into<Value>) -> &mut Self {
59 self.default = Some(value.into());
60 self
61 }
62
63 pub fn length(&mut self, length: u32) -> &mut Self {
65 if let ColumnType::String(n) = &mut self.ty {
66 *n = length;
67 }
68 self
69 }
70
71 pub fn precision(&mut self, precision: u32) -> &mut Self {
79 if let ColumnType::DateTime(p) | ColumnType::Timestamp(p) = &mut self.ty {
80 *p = precision;
81 }
82 self
83 }
84
85 pub fn use_current(&mut self) -> &mut Self {
90 self.use_current = true;
91 self
92 }
93
94 pub fn primary(&mut self) -> &mut Self {
101 self.primary = true;
102 self
103 }
104
105 pub fn change(&mut self) -> &mut Self {
111 self.change = true;
112 self
113 }
114
115 pub(crate) fn render(&self, driver: Driver, dialect: Dialect) -> Result<String, Error> {
117 crate::query::validate(&self.name)?;
118 let name = driver.quote(&self.name);
119 if self.ty == ColumnType::Id {
120 let id = match dialect {
121 Dialect::MySql => "BIGINT AUTO_INCREMENT PRIMARY KEY",
122 Dialect::Postgres => "BIGSERIAL PRIMARY KEY",
124 Dialect::Sqlite => "INTEGER PRIMARY KEY AUTOINCREMENT",
125 };
126 return Ok(format!("{name} {id}"));
127 }
128 let mut sql = format!("{name} {}", self.sql_type(dialect));
129 sql.push_str(if self.nullable { " NULL" } else { " NOT NULL" });
130 if let Some(default) = self.default_sql(driver, dialect)? {
131 sql.push_str(" DEFAULT ");
132 sql.push_str(&default);
133 }
134 if self.primary {
135 sql.push_str(" PRIMARY KEY");
136 }
137 Ok(sql)
138 }
139
140 pub(crate) fn render_change_postgres(&self, driver: Driver) -> Result<String, Error> {
143 crate::query::validate(&self.name)?;
144 let name = driver.quote(&self.name);
145 let ty = self.sql_type(Dialect::Postgres);
146 let null = if self.nullable {
147 "DROP NOT NULL"
148 } else {
149 "SET NOT NULL"
150 };
151 let default = match self.default_sql(driver, Dialect::Postgres)? {
152 Some(default) => format!("SET DEFAULT {default}"),
153 None => "DROP DEFAULT".to_owned(),
154 };
155 Ok(format!(
156 "ALTER COLUMN {name} TYPE {ty}, ALTER COLUMN {name} {null}, \
157 ALTER COLUMN {name} {default}"
158 ))
159 }
160
161 fn default_sql(&self, driver: Driver, dialect: Dialect) -> Result<Option<String>, Error> {
164 if self.use_current {
165 match (self.ty, dialect) {
166 (ColumnType::DateTime(p) | ColumnType::Timestamp(p), Dialect::MySql) if p > 0 => {
167 return Ok(Some(format!("CURRENT_TIMESTAMP({p})")));
168 }
169 (ColumnType::DateTime(_) | ColumnType::Timestamp(_), _) => {
170 return Ok(Some("CURRENT_TIMESTAMP".to_owned()));
171 }
172 _ => {}
173 }
174 }
175 self.default
176 .as_ref()
177 .map(|value| literal(value, driver, dialect))
178 .transpose()
179 }
180
181 fn sql_type(&self, dialect: Dialect) -> String {
182 match (self.ty, dialect) {
183 (ColumnType::Id, _) => unreachable!("an id column is rendered whole"),
184 (ColumnType::String(_), Dialect::Sqlite) => "TEXT".to_owned(),
185 (ColumnType::String(n), _) => format!("VARCHAR({n})"),
186 (ColumnType::Text, _) => "TEXT".to_owned(),
187 (ColumnType::MediumText, Dialect::MySql) => "MEDIUMTEXT".to_owned(),
188 (ColumnType::LongText, Dialect::MySql) => "LONGTEXT".to_owned(),
189 (ColumnType::MediumText | ColumnType::LongText, _) => "TEXT".to_owned(),
190 (ColumnType::Integer, _) => "INTEGER".to_owned(),
191 (ColumnType::BigInteger, Dialect::Sqlite) => "INTEGER".to_owned(),
192 (ColumnType::BigInteger, _) => "BIGINT".to_owned(),
193 (ColumnType::Boolean, Dialect::Postgres) => "BOOLEAN".to_owned(),
194 (ColumnType::Boolean, _) => "TINYINT(1)".to_owned(),
195 (ColumnType::DateTime(p) | ColumnType::Timestamp(p), Dialect::Postgres) => {
196 format!("TIMESTAMP({p}) WITHOUT TIME ZONE")
197 }
198 (ColumnType::DateTime(_) | ColumnType::Timestamp(_), Dialect::Sqlite) => {
199 "DATETIME".to_owned()
200 }
201 (ColumnType::DateTime(0), _) => "DATETIME".to_owned(),
202 (ColumnType::DateTime(p), _) => format!("DATETIME({p})"),
203 (ColumnType::Timestamp(0), _) => "TIMESTAMP".to_owned(),
204 (ColumnType::Timestamp(p), _) => format!("TIMESTAMP({p})"),
205 (ColumnType::Binary, Dialect::Postgres) => "BYTEA".to_owned(),
206 (ColumnType::Binary, _) => "BLOB".to_owned(),
207 }
208 }
209}
210
211fn literal(value: &Value, driver: Driver, dialect: Dialect) -> Result<String, Error> {
213 let unsupported = || Error::Unsupported {
214 operation: "a column default of this type",
215 driver: driver.name(),
216 };
217 Ok(match value {
218 Value::Null => "NULL".to_owned(),
219 Value::Bool(b) => match (dialect, b) {
220 (Dialect::Postgres, true) => "true".to_owned(),
221 (Dialect::Postgres, false) => "false".to_owned(),
222 (_, true) => "1".to_owned(),
223 (_, false) => "0".to_owned(),
224 },
225 Value::Int(n) => n.to_string(),
226 Value::UInt(n) => n.to_string(),
227 Value::Float(f) if f.is_finite() => format!("{f:?}"),
228 Value::Float(_) => return Err(unsupported()),
229 Value::Text(s) => quote_text(s, dialect),
230 Value::DateTime(d) => quote_text(&format_datetime(d), dialect),
231 Value::Date(d) => quote_text(&format_date(d), dialect),
232 Value::Bytes(_) => return Err(unsupported()),
233 })
234}
235
236fn format_datetime(d: &NaiveDateTime) -> String {
237 d.format("%Y-%m-%d %H:%M:%S").to_string()
238}
239
240fn format_date(d: &NaiveDate) -> String {
241 d.format("%Y-%m-%d").to_string()
242}
243
244fn quote_text(s: &str, dialect: Dialect) -> String {
247 let mut out = s.replace('\'', "''");
248 if dialect == Dialect::MySql {
249 out = out.replace('\\', "\\\\");
250 }
251 format!("'{out}'")
252}