///|
/// A column's SQL storage type. These map onto SQLite's type affinities when a
/// `Model` renders `CREATE TABLE` DDL: `IntType`/`BoolType` -> INTEGER, `TextType`
/// -> TEXT, `RealType` -> REAL, `BlobType` -> BLOB. `BoolType` is a distinct
/// declared type so DDL reads intently, but binds and reads as an integer `0`/`1`.
pub(all) enum ColumnType {
IntType
TextType
RealType
BlobType
BoolType
// SQLModel's everyday field types beyond the storage affinities. `VarcharType(n)`
// carries a length; the rest declare a type SQLite tolerates and that a dialect
// renders natively (see `ColumnType::sql`). UUID/JSON store as TEXT on SQLite.
VarcharType(Int32)
NumericType
DateTimeType
DateType
TimeType
UuidType
JsonType
} derive(Eq)
///|
/// The SQLite declared type keyword for a `ColumnType`.
fn ColumnType::sql(self : ColumnType) -> String {
match self {
IntType => "INTEGER"
BoolType => "INTEGER"
TextType => "TEXT"
RealType => "REAL"
BlobType => "BLOB"
VarcharType(n) => "VARCHAR(" + n.to_string() + ")"
NumericType => "NUMERIC"
DateTimeType => "DATETIME"
DateType => "DATE"
TimeType => "TIME"
// SQLite has no native UUID/JSON storage class; they live as TEXT (a dialect
// renders UUID / JSONB natively — see sql_for).
UuidType => "TEXT"
JsonType => "TEXT"
}
}
///|
/// One declared column of a `Model`: its `name`, storage `col_type`, whether it is
/// the (or part of the) primary key, whether it accepts NULL, and an optional
/// foreign-key `references` target `(table, column)`. This is the explicit stand-in
/// for SQLAlchemy's `mapped_column(...)` — every fact that framework reads off the
/// annotated attribute is stated here as data.
pub(all) struct Column {
name : String
col_type : ColumnType
primary_key : Bool
// A server-generated key (SQLAlchemy `autoincrement` / SERIAL / AUTO_INCREMENT).
// On SQLite, an autoincrement primary key renders `INTEGER PRIMARY KEY AUTOINCREMENT`.
autoincrement : Bool
nullable : Bool
unique : Bool
// A DDL `DEFAULT` expression (SQLAlchemy `server_default`), e.g. `"0"` or
// `"CURRENT_TIMESTAMP"`. A trusted SQL literal, emitted verbatim, never bound.
default : String?
// Column-level `CHECK (expr)` constraints (SQLAlchemy CheckConstraint / column
// `CHECK`), each a trusted SQL expression.
checks : Array[String]
references : (String, String)?
// Referential actions on the foreign key (SQLAlchemy ForeignKey ondelete/onupdate),
// e.g. `"CASCADE"` / `"SET NULL"` / `"RESTRICT"`. Emitted only when `references` is
// set.
on_delete : String?
on_update : String?
}
///|
/// Declare a `Column`. `primary_key` and `references` default off; a primary-key
/// column is `NOT NULL` implicitly (SQLite treats INTEGER PRIMARY KEY as the
/// rowid), and `nullable` defaults to `true` for every other column, matching
/// SQLAlchemy's `nullable=True` default.
pub fn column(
name : String,
col_type : ColumnType,
primary_key~ : Bool = false,
autoincrement~ : Bool = false,
nullable~ : Bool = true,
unique~ : Bool = false,
default? : String,
checks~ : Array[String] = [],
references? : (String, String),
on_delete? : String,
on_update? : String,
) -> Column {
{
name,
col_type,
primary_key,
autoincrement,
nullable,
unique,
default,
checks,
references,
on_delete,
on_update,
}
}
///|
/// Render this column as a single `CREATE TABLE` column definition, e.g.
/// `"user_id INTEGER NOT NULL REFERENCES users(id)"`. A `PRIMARY KEY` column emits
/// that keyword; a non-nullable column emits `NOT NULL`; a `references` target
/// emits a `REFERENCES table(column)` clause. Column and table names are trusted
/// identifiers declared in code, never bound values.
pub fn Column::ddl(self : Column, inline_pk~ : Bool = true) -> String {
let mut s = self.name + " " + self.col_type.sql()
if self.primary_key && inline_pk {
s = s + " PRIMARY KEY"
if self.autoincrement {
s = s + " AUTOINCREMENT"
}
} else if self.primary_key || !self.nullable {
// A primary-key column is NOT NULL; when the key is a table-level constraint
// (composite key, `inline_pk=false`) the column still carries NOT NULL.
s = s + " NOT NULL"
}
if self.unique && !self.primary_key {
s = s + " UNIQUE"
}
if self.default is Some(expr) {
s = s + " DEFAULT " + expr
}
for chk in self.checks {
s = s + " CHECK (" + chk + ")"
}
match self.references {
Some((t, c)) => {
s = s + " REFERENCES " + t + "(" + c + ")"
if self.on_delete is Some(act) {
s = s + " ON DELETE " + act
}
if self.on_update is Some(act) {
s = s + " ON UPDATE " + act
}
}
None => ()
}
s
}
///|
/// The declared type keyword for a `ColumnType` in a given SQL `Dialect`. SQLite is
/// the affinity-based default; PostgreSQL and MySQL render their native spellings
/// (BOOLEAN/BYTEA/DOUBLE PRECISION/UUID/JSONB on PG; INT/TINYINT(1)/DOUBLE/JSON on
/// MySQL). This is the per-dialect counterpart of `ColumnType::sql`.
pub fn ColumnType::sql_for(
self : ColumnType,
dialect : @query.Dialect,
) -> String {
match (self, dialect) {
(IntType, Mysql) => "INT"
(IntType, _) => "INTEGER"
(BoolType, Postgres) => "BOOLEAN"
(BoolType, Mysql) => "TINYINT(1)"
(BoolType, _) => "INTEGER"
(RealType, Postgres) => "DOUBLE PRECISION"
(RealType, Mysql) => "DOUBLE"
(RealType, _) => "REAL"
(BlobType, Postgres) => "BYTEA"
(BlobType, _) => "BLOB"
(VarcharType(n), _) => "VARCHAR(" + n.to_string() + ")"
(NumericType, _) => "NUMERIC"
(DateTimeType, Postgres) => "TIMESTAMP"
(DateTimeType, _) => "DATETIME"
(DateType, _) => "DATE"
(TimeType, _) => "TIME"
(UuidType, Postgres) => "UUID"
(UuidType, Mysql) => "CHAR(36)"
(UuidType, _) => "TEXT"
(JsonType, Postgres) => "JSONB"
(JsonType, Mysql) => "JSON"
(JsonType, _) => "TEXT"
(TextType, _) => "TEXT"
}
}
///|
/// A column definition rendered for a specific `Dialect`. An autoincrement primary
/// key takes each engine's idiom: SQLite `INTEGER PRIMARY KEY AUTOINCREMENT`,
/// PostgreSQL `SERIAL PRIMARY KEY`, MySQL `INT AUTO_INCREMENT PRIMARY KEY`. Otherwise
/// it is `sql_for` plus the same constraint clauses as `ddl`.
pub fn Column::ddl_for(
self : Column,
dialect : @query.Dialect,
inline_pk~ : Bool = true,
) -> String {
let mut s = if self.primary_key && inline_pk && self.autoincrement {
match dialect {
Postgres => self.name + " SERIAL PRIMARY KEY"
Mysql => self.name + " INT AUTO_INCREMENT PRIMARY KEY"
Sqlite => self.name + " INTEGER PRIMARY KEY AUTOINCREMENT"
}
} else {
let mut d = self.name + " " + self.col_type.sql_for(dialect)
if self.primary_key && inline_pk {
d = d + " PRIMARY KEY"
} else if self.primary_key || !self.nullable {
d = d + " NOT NULL"
}
d
}
if self.unique && !self.primary_key {
s = s + " UNIQUE"
}
if self.default is Some(expr) {
s = s + " DEFAULT " + expr
}
for chk in self.checks {
s = s + " CHECK (" + chk + ")"
}
match self.references {
Some((t, c)) => {
s = s + " REFERENCES " + t + "(" + c + ")"
if self.on_delete is Some(act) {
s = s + " ON DELETE " + act
}
if self.on_update is Some(act) {
s = s + " ON UPDATE " + act
}
}
None => ()
}
s
}
///|
pub extend ColumnType with Eq::{not_equal, equal}