1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/// Safe values allowed for a column DEFAULT clause.
///
/// Accepting a raw `&str` would allow DDL injection through the DEFAULT
/// position. This enum restricts callers to known-safe literals.
#[derive(Debug, Clone, PartialEq)]
pub enum ColumnDefault {
/// `CURRENT_TIMESTAMP` — standard SQL timestamp literal.
CurrentTimestamp,
/// `NULL` — explicit SQL null default.
Null,
/// A non-negative integer literal (e.g. `0`, `1`).
Integer(i64),
/// A non-negative real literal (e.g. `0.0`).
Float(f64),
/// A string literal that will be single-quoted, with embedded single
/// quotes doubled. Building the schema rejects text containing a
/// backslash or a control character: MySQL/MariaDB treat a backslash as an
/// escape character inside quoted literals by default, so it could escape
/// the closing quote.
Text(String),
}
impl ColumnDefault {
/// Renders the default value as a SQL fragment.
///
/// This does not validate [`ColumnDefault::Text`]; the schema builder does
/// before it emits DDL.
pub fn to_sql(&self) -> String {
match self {
ColumnDefault::CurrentTimestamp => "CURRENT_TIMESTAMP".to_string(),
ColumnDefault::Null => "NULL".to_string(),
ColumnDefault::Integer(n) => n.to_string(),
ColumnDefault::Float(f) => format!("{f}"),
// Single-quote the string and escape any embedded single-quotes
// via SQL standard doubling (''), which is safe on every driver.
ColumnDefault::Text(s) => format!("'{}'", s.replace('\'', "''")),
}
}
}
/// Rejects text that cannot be embedded as a portable single-quoted DDL
/// literal. Single quotes are doubled by the caller; a backslash would escape
/// the closing quote on MySQL/MariaDB, and control characters have no portable
/// literal form.
pub(super) fn validate_text_literal(value: &str, context: &str) -> Result<(), crate::Error> {
if value
.chars()
.any(|character| character == '\\' || character.is_control())
{
return Err(crate::Error::Validation(format!(
"{context} must not contain backslashes or control characters"
)));
}
Ok(())
}
pub struct Column {
pub name: String,
pub col_type: String,
pub is_nullable: bool,
pub is_primary_key: bool,
pub is_auto_increment: bool,
pub default_value: Option<ColumnDefault>,
/// Emits a column-level `UNIQUE` constraint. Added in v13.
pub is_unique: bool,
}
impl Column {
/// Creates a new column.
///
/// Identifier validation is repeated by the fallible schema builder before
/// SQL is emitted, including for callers that mutate the public fields.
pub fn new(name: impl Into<String>, col_type: impl Into<String>) -> Self {
Self {
name: name.into(),
col_type: col_type.into(),
is_nullable: true,
is_primary_key: false,
is_auto_increment: false,
default_value: None,
is_unique: false,
}
}
pub fn not_null(&mut self) -> &mut Self {
self.is_nullable = false;
self
}
pub fn nullable(&mut self) -> &mut Self {
self.is_nullable = true;
self
}
/// Sets a safe DEFAULT value using the [`ColumnDefault`] enum.
///
/// The old `&str` overload has been removed to prevent DDL injection
/// through unescaped DEFAULT clauses.
pub fn default(&mut self, val: ColumnDefault) -> &mut Self {
self.default_value = Some(val);
self
}
pub fn primary(&mut self) -> &mut Self {
self.is_primary_key = true;
self
}
/// Adds a column-level `UNIQUE` constraint; the database names it
/// (PostgreSQL: `<table>_<column>_key`). MySQL/MariaDB cannot make a
/// `TEXT` column unique, so on those a `string()` column needs a bounded
/// type such as `VARCHAR(255)` (set [`Column::col_type`]).
pub fn unique(&mut self) -> &mut Self {
self.is_unique = true;
self
}
}