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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
use super::column::{Column, ColumnDefault, validate_text_literal};
use super::column_types::DriverType;
use super::constraints::{Check, ForeignKey, Index, IndexKind};
use super::enums::{DatabaseEnum, NativeEnumDefinition, validate_native_enum};
use crate::Error;
pub struct Blueprint {
pub columns: Vec<Column>,
pub(super) native_enum_columns: Vec<(String, NativeEnumDefinition)>,
/// `enum_col` columns with a variant that is not a safe DDL literal;
/// reported by `build` because `enum_col` itself cannot fail.
pub(super) invalid_enum_columns: Vec<String>,
/// Columns whose DDL type is chosen per driver when the schema is built.
driver_typed_columns: Vec<(String, DriverType)>,
pub(super) indexes: Vec<Index>,
pub(super) foreign_keys: Vec<ForeignKey>,
pub(super) checks: Vec<Check>,
}
impl Default for Blueprint {
fn default() -> Self {
Self::new()
}
}
impl Blueprint {
pub fn new() -> Self {
Self {
columns: vec![],
native_enum_columns: vec![],
invalid_enum_columns: vec![],
driver_typed_columns: vec![],
indexes: vec![],
foreign_keys: vec![],
checks: vec![],
}
}
fn add_driver_typed_column(&mut self, name: &str, kind: DriverType) -> &mut Column {
self.driver_typed_columns.push((name.to_string(), kind));
self.add_column(name, kind.portable())
}
/// The driver-specific type of a column added by a typed helper, unless
/// the caller has since replaced its `col_type`.
pub(super) fn driver_type(&self, col: &Column) -> Option<DriverType> {
self.driver_typed_columns
.iter()
.rev()
.find(|(name, _)| name == &col.name)
.map(|(_, kind)| *kind)
.filter(|kind| col.col_type == kind.portable())
}
pub fn id(&mut self) -> &mut Column {
self.columns.push(Column {
name: "id".to_string(),
col_type: "INTEGER".to_string(),
is_nullable: false,
is_primary_key: true,
is_auto_increment: true,
default_value: None,
is_unique: false,
});
let len = self.columns.len();
&mut self.columns[len - 1]
}
fn add_column(&mut self, name: &str, col_type: &str) -> &mut Column {
let col = Column::new(name, col_type);
self.columns.push(col);
let len = self.columns.len();
&mut self.columns[len - 1]
}
pub fn string(&mut self, name: &str) -> &mut Column {
self.add_column(name, "TEXT")
}
pub fn integer(&mut self, name: &str) -> &mut Column {
self.add_column(name, "INTEGER")
}
pub fn big_integer(&mut self, name: &str) -> &mut Column {
self.add_column(name, "BIGINT")
}
/// Adds an `f64` column: `DOUBLE PRECISION` on PostgreSQL, `DOUBLE` on
/// MySQL/MariaDB and `REAL` (8-byte) on SQLite.
///
/// [`Column::col_type`] reads `REAL` until the schema is built; replacing
/// it keeps the replacement on every driver.
pub fn float(&mut self, name: &str) -> &mut Column {
self.add_driver_typed_column(name, DriverType::Double)
}
/// Adds a `bool` column: `BOOLEAN` on PostgreSQL and an `INTEGER` 0/1
/// flag on MySQL/MariaDB and SQLite.
///
/// On PostgreSQL `ColumnDefault::Integer(0)`/`(1)` render as
/// `FALSE`/`TRUE`; any other integer default fails the build.
/// [`Column::col_type`] reads `INTEGER` until the schema is built;
/// replacing it keeps the replacement on every driver.
pub fn boolean(&mut self, name: &str) -> &mut Column {
self.add_driver_typed_column(name, DriverType::Boolean)
}
pub fn vector(&mut self, name: &str, dimensions: usize) -> &mut Column {
let col_type = format!("VECTOR({})", dimensions);
self.add_column(name, &col_type)
}
/// Adds a calendar-date column for a `chrono::NaiveDate` field: `DATE` on
/// PostgreSQL and MySQL/MariaDB, ISO-8601 `TEXT` (`YYYY-MM-DD`) on SQLite.
///
/// PostgreSQL and MySQL/MariaDB date/time columns need a strict driver
/// feature (`strict-postgres`, `strict-mysql`): `Schema::create` rejects
/// them on the SQLx `Any` pool, which cannot read them. A
/// `ColumnDefault::CurrentTimestamp` default is today's UTC date.
/// Added in v13.
pub fn date(&mut self, name: &str) -> &mut Column {
self.add_driver_typed_column(name, DriverType::Date)
}
/// Adds a time-of-day column for a `chrono::NaiveTime` field: `TIME` on
/// PostgreSQL, `TIME(6)` on MySQL/MariaDB and `TEXT` (`HH:MM:SS[.f]`) on
/// SQLite. See [`Blueprint::date`] for the driver requirement. Added in v13.
pub fn time(&mut self, name: &str) -> &mut Column {
self.add_driver_typed_column(name, DriverType::Time)
}
/// Adds a date-and-time column without a zone for a
/// `chrono::NaiveDateTime` field: `TIMESTAMP` on PostgreSQL,
/// `DATETIME(6)` on MySQL/MariaDB and `TEXT` (`YYYY-MM-DD HH:MM:SS[.f]`)
/// on SQLite. A `ColumnDefault::CurrentTimestamp` default is the current
/// UTC wall time. See [`Blueprint::date`] for the driver requirement.
/// Added in v13.
pub fn timestamp(&mut self, name: &str) -> &mut Column {
self.add_driver_typed_column(name, DriverType::Timestamp)
}
/// Adds a UTC-instant column for a `chrono::DateTime<Utc>` field:
/// `TIMESTAMPTZ` on PostgreSQL, `DATETIME(6)` holding UTC on
/// MySQL/MariaDB (whose `TIMESTAMP` ends in 2038; SQLx sets the session
/// time zone to `+00:00`) and RFC 3339 `TEXT`
/// (`YYYY-MM-DDTHH:MM:SS[.f]+00:00`, which sorts as text) on SQLite. See
/// [`Blueprint::date`] for the driver requirement. Added in v13.
pub fn timestamp_tz(&mut self, name: &str) -> &mut Column {
self.add_driver_typed_column(name, DriverType::TimestampTz)
}
/// Adds a `TEXT` column restricted to `variants` by a `CHECK` constraint.
///
/// Single quotes in a variant are doubled. A variant containing a
/// backslash or a control character makes [`Blueprint::build`] fail.
pub fn enum_col(&mut self, name: &str, variants: Vec<&str>) -> &mut Column {
if variants
.iter()
.any(|variant| validate_text_literal(variant, name).is_err())
{
self.invalid_enum_columns.push(name.to_string());
}
// Enforce enum values using a CHECK constraint for safe cross-DB compatibility
let check_clause = variants
.iter()
.map(|v| format!("'{}'", v.replace('\'', "''")))
.collect::<Vec<_>>()
.join(", ");
let col_type = format!("TEXT CHECK({} IN ({}))", name, check_clause);
self.add_column(name, &col_type)
}
/// Adds a database-native enum column from a closed [`DatabaseEnum`]
/// contract.
///
/// PostgreSQL uses the named enum type (created and drift-checked by
/// [`Schema::create`](super::Schema::create)); MySQL/MariaDB use an inline
/// `ENUM`, and SQLite uses `TEXT CHECK`. Metadata is validated when the
/// blueprint is built, so a manually implemented invalid contract fails
/// before any table DDL is executed.
pub fn native_enum<E: DatabaseEnum>(&mut self, name: &str) -> &mut Column {
self.native_enum_columns.push((
name.to_string(),
NativeEnumDefinition {
type_name: E::TYPE_NAME,
variants: E::VARIANTS,
},
));
self.add_column(name, "TEXT")
}
/// Adds nullable `created_at`/`updated_at` columns that default to the
/// database's current time.
///
/// With a strict driver feature (`strict-postgres`, `strict-mysql`,
/// `strict-sqlite`, which generated projects use) they are
/// [`Blueprint::timestamp_tz`] columns for `chrono::DateTime<Utc>` fields:
/// `TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP` on PostgreSQL,
/// `DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)` on MySQL/MariaDB and RFC 3339
/// `TEXT` on SQLite. Since v13; alpha.1 created `TEXT` columns.
///
/// The SQLx `Any` pool (the `drivers-all` default without a strict
/// feature) cannot read date/time columns, so there they stay the
/// alpha.1 `TEXT DEFAULT CURRENT_TIMESTAMP` columns for `String` fields
/// (`DEFAULT (CURRENT_TIMESTAMP)` on MySQL/MariaDB).
pub fn timestamps(&mut self) {
for name in ["created_at", "updated_at"] {
self.add_driver_typed_column(name, DriverType::AutoTimestamp)
.default(ColumnDefault::CurrentTimestamp);
}
}
pub fn soft_deletes(&mut self) {
let col = Column::new("deleted_at", "TEXT");
self.columns.push(col);
let len = self.columns.len();
self.columns[len - 1].nullable();
}
/// Adds a table-level `UNIQUE (columns)` constraint, named
/// `<table>_<columns>_unique` unless [`Index::name`] sets one. It is
/// part of `CREATE TABLE`; MySQL/MariaDB cannot make a `TEXT` column
/// unique (see [`Column::unique`]). Added in v13.
pub fn unique(&mut self, columns: &[&str]) -> &mut Index {
self.add_index(columns, IndexKind::UniqueConstraint)
}
/// Adds an index named `<table>_<columns>_index` unless [`Index::name`]
/// sets one: `CREATE INDEX IF NOT EXISTS` after `CREATE TABLE` on
/// PostgreSQL and SQLite, an `INDEX` clause of `CREATE TABLE` on
/// MySQL/MariaDB, which cannot index a `TEXT` column. Added in v13.
pub fn index(&mut self, columns: &[&str]) -> &mut Index {
self.add_index(columns, IndexKind::Plain)
}
/// Like [`Blueprint::index`] for a `UNIQUE` index, named
/// `<table>_<columns>_unique` by default. Added in v13.
pub fn unique_index(&mut self, columns: &[&str]) -> &mut Index {
self.add_index(columns, IndexKind::Unique)
}
fn add_index(&mut self, columns: &[&str], kind: IndexKind) -> &mut Index {
self.indexes.push(Index::new(columns, kind));
let len = self.indexes.len();
&mut self.indexes[len - 1]
}
/// Adds a foreign key on an existing column of this blueprint, named
/// `<table>_<column>_foreign` unless [`ForeignKey::name`] sets one:
///
/// ```rust,no_run
/// # use rullst_orm::schema::{Blueprint, ForeignKeyAction};
/// # fn up(table: &mut Blueprint) {
/// table.integer("user_id").not_null();
/// table
/// .foreign("user_id")
/// .references("users", "id")
/// .on_delete(ForeignKeyAction::Cascade);
/// # }
/// ```
///
/// The column type must match the referenced one (`id()` is `INTEGER`).
/// SQLite enforces foreign keys only with `PRAGMA foreign_keys = ON`,
/// which SQLx enables on every connection it opens. Added in v13.
pub fn foreign(&mut self, column: impl Into<String>) -> &mut ForeignKey {
self.foreign_keys.push(ForeignKey::new(column.into()));
let len = self.foreign_keys.len();
&mut self.foreign_keys[len - 1]
}
/// Adds an `INTEGER NOT NULL` column and a foreign key from it to
/// `table(id)`, the key of [`Blueprint::id`]:
/// `table.foreign_id("user_id", "users").on_delete(ForeignKeyAction::Cascade)`.
/// For a nullable key, declare the column with [`Blueprint::integer`] and
/// use [`Blueprint::foreign`]. Added in v13.
pub fn foreign_id(
&mut self,
column: impl Into<String>,
table: impl Into<String>,
) -> &mut ForeignKey {
let column = column.into();
self.integer(&column).not_null();
let foreign = self.foreign(column);
foreign.references(table, "id");
foreign
}
/// Adds a table-level `CHECK (expression)` constraint.
///
/// The expression is trusted developer SQL: it is emitted as written, so
/// never build it from request data. Building fails when it is empty,
/// longer than 512 bytes, contains `;`, a comment, `\`, `$`, a backtick
/// or a control character, or has unbalanced parentheses or quotes.
/// MySQL enforces CHECK from 8.0.16 and MariaDB from 10.2.1; older
/// servers parse and ignore it. Added in v13.
pub fn check(&mut self, expression: impl Into<String>) -> &mut Check {
self.checks.push(Check::new(expression.into()));
let len = self.checks.len();
&mut self.checks[len - 1]
}
/// The column definitions and table constraints of a `CREATE TABLE`
/// statement for the connected driver. Indexes that PostgreSQL and SQLite
/// create with separate statements are not included; constraint names
/// that default to `<table>_...` are left to the database.
#[cfg_attr(test, mutants::skip)]
pub fn build(&self) -> Result<String, Error> {
self.build_for_driver(crate::Orm::driver()?)
}
pub(crate) fn build_for_driver(&self, driver: &str) -> Result<String, Error> {
self.render(None, driver, crate::database::ANY_POOL)
}
pub(super) fn postgres_enum_definitions(&self) -> Result<Vec<NativeEnumDefinition>, Error> {
let mut definitions = std::collections::BTreeMap::new();
for (_, definition) in &self.native_enum_columns {
validate_native_enum(definition.type_name, definition.variants)?;
if let Some(existing) = definitions.get(definition.type_name) {
if existing != definition {
return Err(Error::Validation(format!(
"database enum type `{}` has conflicting label definitions",
definition.type_name
)));
}
} else {
definitions.insert(definition.type_name, definition.clone());
}
}
Ok(definitions.into_values().collect())
}
}