Skip to main content

orbit_orm/schema/
column.rs

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/// A column of a table being created. Chain `nullable`, `default` and `length` on it.
18#[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    /// Lets the column hold `NULL`. Columns are `NOT NULL` otherwise, as in Laravel.
37    pub fn nullable(&mut self) -> &mut Self {
38        self.nullable = true;
39        self
40    }
41
42    /// Sets the default value, written into the SQL as a literal.
43    pub fn default(&mut self, value: impl Into<Value>) -> &mut Self {
44        self.default = Some(value.into());
45        self
46    }
47
48    /// Sets the length of a `string` column. It has no effect on other types.
49    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    /// The column's SQL: its quoted name, type, nullability and default.
57    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                // BIGSERIAL, as Laravel's PostgreSQL grammar writes `bigIncrements`.
64                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
97/// A default value as a SQL literal. DDL cannot take bound values.
98fn 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
130/// A quoted string literal. A quote is doubled everywhere; MySQL also reads a backslash as an
131/// escape, so it is doubled there.
132fn 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}