use sea_query::{
Alias, ColumnDef, ForeignKey, ForeignKeyAction, Index, IndexCreateStatement, Table,
TableCreateStatement, Value,
};
use crate::{schema, ColumnType, Dialect};
pub const DEFAULT_STRING_LEN: u32 = 255;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ColumnKind {
Basic(ColumnType),
Varchar(u32),
Json,
Decimal {
precision: u32,
scale: u32,
},
}
impl ColumnKind {
fn apply(&self, def: &mut ColumnDef) {
match self {
ColumnKind::Basic(ty) => schema::apply_type(def, *ty, false),
ColumnKind::Varchar(len) => {
def.string_len(*len);
}
ColumnKind::Json => {
def.json_binary();
}
ColumnKind::Decimal { precision, scale } => {
def.decimal_len(*precision, *scale);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Cascade,
Restrict,
SetNull,
NoAction,
}
impl Action {
fn to_sea(self) -> ForeignKeyAction {
match self {
Action::Cascade => ForeignKeyAction::Cascade,
Action::Restrict => ForeignKeyAction::Restrict,
Action::SetNull => ForeignKeyAction::SetNull,
Action::NoAction => ForeignKeyAction::NoAction,
}
}
}
#[derive(Debug, Clone)]
pub struct Column {
name: String,
kind: ColumnKind,
nullable: bool,
default: Option<Value>,
auto_increment: bool,
primary: bool,
unique: bool,
indexed: bool,
references: Option<(String, String, Option<Action>, Option<Action>)>,
}
impl Column {
fn new(name: impl Into<String>, kind: ColumnKind) -> Self {
Self {
name: name.into(),
kind,
nullable: false,
default: None,
auto_increment: false,
primary: false,
unique: false,
indexed: false,
references: None,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn nullable(&mut self) -> &mut Self {
self.nullable = true;
self
}
pub fn default(&mut self, value: impl Into<Value>) -> &mut Self {
self.default = Some(value.into());
self
}
pub fn primary(&mut self) -> &mut Self {
self.primary = true;
self
}
pub fn auto_increment(&mut self) -> &mut Self {
self.auto_increment = true;
self.primary = true;
self
}
pub fn unique(&mut self) -> &mut Self {
self.unique = true;
self
}
pub fn index(&mut self) -> &mut Self {
self.indexed = true;
self
}
pub fn constrained_on(&mut self, table: impl Into<String>) -> &mut Self {
self.references = Some((table.into(), "id".to_string(), None, None));
self
}
pub fn references(&mut self, table: impl Into<String>, column: impl Into<String>) -> &mut Self {
self.references = Some((table.into(), column.into(), None, None));
self
}
pub fn cascade_on_delete(&mut self) -> &mut Self {
self.on_delete(Action::Cascade)
}
pub fn null_on_delete(&mut self) -> &mut Self {
self.on_delete(Action::SetNull)
}
pub fn on_delete(&mut self, action: Action) -> &mut Self {
if let Some(reference) = &mut self.references {
reference.2 = Some(action);
}
self
}
pub fn on_update(&mut self, action: Action) -> &mut Self {
if let Some(reference) = &mut self.references {
reference.3 = Some(action);
}
self
}
fn to_def(&self) -> ColumnDef {
let mut def = ColumnDef::new(Alias::new(self.name.clone()));
let kind = match (self.auto_increment, self.kind) {
(true, ColumnKind::Basic(ColumnType::BigUint)) => ColumnKind::Basic(ColumnType::BigInt),
(true, ColumnKind::Basic(ColumnType::Uint)) => ColumnKind::Basic(ColumnType::Int),
(_, kind) => kind,
};
kind.apply(&mut def);
if self.primary {
def.primary_key();
}
if self.auto_increment {
def.auto_increment();
}
if self.nullable {
def.null();
} else {
def.not_null();
}
if let Some(value) = &self.default {
def.default(value.clone());
}
if self.unique {
def.unique_key();
}
def
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexDef {
name: String,
columns: Vec<String>,
unique: bool,
}
impl IndexDef {
pub fn name(&self) -> &str {
&self.name
}
pub fn columns(&self) -> &[String] {
&self.columns
}
pub fn is_unique(&self) -> bool {
self.unique
}
pub fn named(&mut self, name: impl Into<String>) -> &mut Self {
self.name = name.into();
self
}
fn to_stmt(&self, table: &str) -> IndexCreateStatement {
let mut stmt = Index::create();
stmt.name(&self.name).table(Alias::new(table)).if_not_exists();
if self.unique {
stmt.unique();
}
for column in &self.columns {
stmt.col(Alias::new(column.clone()));
}
stmt
}
}
fn conventional_name(table: &str, columns: &[String], suffix: &str) -> String {
format!("{table}_{}_{suffix}", columns.join("_"))
}
#[derive(Debug, Clone)]
pub struct Blueprint {
table: String,
columns: Vec<Column>,
indexes: Vec<IndexDef>,
primary: Option<Vec<String>>,
if_not_exists: bool,
}
impl Blueprint {
pub fn new(table: impl Into<String>) -> Self {
Self {
table: table.into(),
columns: Vec::new(),
indexes: Vec::new(),
primary: None,
if_not_exists: true,
}
}
pub fn create(table: impl Into<String>, build: impl FnOnce(&mut Blueprint)) -> Self {
let mut blueprint = Self::new(table);
build(&mut blueprint);
blueprint
}
pub fn table(&self) -> &str {
&self.table
}
pub fn strict(&mut self) -> &mut Self {
self.if_not_exists = false;
self
}
pub fn column(&mut self, name: impl Into<String>, kind: ColumnKind) -> &mut Column {
self.columns.push(Column::new(name, kind));
self.columns.last_mut().expect("just pushed")
}
pub fn id(&mut self) -> &mut Column {
self.big_increments("id")
}
pub fn big_increments(&mut self, name: impl Into<String>) -> &mut Column {
let column = self.column(name, ColumnKind::Basic(ColumnType::BigUint));
column.auto_increment();
column
}
pub fn increments(&mut self, name: impl Into<String>) -> &mut Column {
let column = self.column(name, ColumnKind::Basic(ColumnType::Uint));
column.auto_increment();
column
}
pub fn string(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Varchar(DEFAULT_STRING_LEN))
}
pub fn string_len(&mut self, name: impl Into<String>, len: u32) -> &mut Column {
self.column(name, ColumnKind::Varchar(len))
}
pub fn text(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Text))
}
pub fn integer(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Int))
}
pub fn big_integer(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::BigInt))
}
pub fn unsigned_integer(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Uint))
}
pub fn unsigned_big_integer(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::BigUint))
}
pub fn foreign_id(&mut self, name: impl Into<String>) -> &mut Column {
let column = self.column(name, ColumnKind::Basic(ColumnType::BigUint));
column.index();
column
}
pub fn boolean(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Bool))
}
pub fn double(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Double))
}
pub fn decimal(&mut self, name: impl Into<String>, precision: u32, scale: u32) -> &mut Column {
self.column(name, ColumnKind::Decimal { precision, scale })
}
pub fn timestamp(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Timestamp))
}
pub fn date(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Date))
}
pub fn binary(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Binary))
}
pub fn json(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Json)
}
pub fn timestamps(&mut self) -> &mut Self {
self.timestamp("created_at").nullable();
self.timestamp("updated_at").nullable();
self
}
pub fn soft_deletes(&mut self) -> &mut Column {
let column = self.timestamp("deleted_at");
column.nullable();
column
}
pub fn primary<I, S>(&mut self, columns: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.primary = Some(columns.into_iter().map(Into::into).collect());
self
}
pub fn index<I, S>(&mut self, columns: I) -> &mut IndexDef
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.push_index(columns, false)
}
pub fn unique<I, S>(&mut self, columns: I) -> &mut IndexDef
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.push_index(columns, true)
}
fn push_index<I, S>(&mut self, columns: I, unique: bool) -> &mut IndexDef
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let columns: Vec<String> = columns.into_iter().map(Into::into).collect();
let suffix = if unique { "unique" } else { "index" };
self.indexes.push(IndexDef {
name: conventional_name(&self.table, &columns, suffix),
columns,
unique,
});
self.indexes.last_mut().expect("just pushed")
}
pub fn to_statement(&self) -> TableCreateStatement {
let mut stmt = Table::create();
stmt.table(Alias::new(self.table.clone()));
if self.if_not_exists {
stmt.if_not_exists();
}
for column in &self.columns {
let mut def = column.to_def();
if self.primary.is_some() {
strip_inline_primary(&mut def, column);
}
stmt.col(&mut def);
}
if let Some(columns) = &self.primary {
let mut key = Index::create();
for column in columns {
key.col(Alias::new(column.clone()));
}
stmt.primary_key(&mut key);
}
for column in &self.columns {
let Some((table, referenced, on_delete, on_update)) = &column.references else {
continue;
};
let mut constraint = ForeignKey::create();
constraint
.name(conventional_name(&self.table, std::slice::from_ref(&column.name), "foreign"))
.from(Alias::new(self.table.clone()), Alias::new(column.name.clone()))
.to(Alias::new(table.clone()), Alias::new(referenced.clone()));
if let Some(action) = on_delete {
constraint.on_delete(action.to_sea());
}
if let Some(action) = on_update {
constraint.on_update(action.to_sea());
}
stmt.foreign_key(&mut constraint);
}
stmt
}
pub fn to_sql(&self, dialect: Dialect) -> Vec<String> {
let mut statements = vec![dialect.build_schema(&self.to_statement())];
for column in &self.columns {
if column.indexed {
let columns = vec![column.name.clone()];
let index = IndexDef {
name: conventional_name(&self.table, &columns, "index"),
columns,
unique: false,
};
statements.push(dialect.build_schema(&index.to_stmt(&self.table)));
}
}
for index in &self.indexes {
statements.push(dialect.build_schema(&index.to_stmt(&self.table)));
}
statements
}
pub fn to_reverse_sql(&self, dialect: Dialect) -> Vec<String> {
let mut stmt = Table::drop();
stmt.table(Alias::new(self.table.clone())).if_exists();
vec![dialect.build_schema(&stmt)]
}
}
#[derive(Debug, Clone)]
pub struct TableChanges {
table: String,
changes: Vec<Change>,
}
#[derive(Debug, Clone)]
enum Change {
AddColumn(Column),
DropColumn(String),
RenameColumn(String, String),
AddIndex(IndexDef),
DropIndex(IndexDef),
Raw(Option<Dialect>, String, Option<String>),
}
impl TableChanges {
pub fn new(table: impl Into<String>) -> Self {
Self { table: table.into(), changes: Vec::new() }
}
pub fn to(table: impl Into<String>, build: impl FnOnce(&mut TableChanges)) -> Self {
let mut changes = Self::new(table);
build(&mut changes);
changes
}
pub fn table(&self) -> &str {
&self.table
}
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
pub fn column(&mut self, name: impl Into<String>, kind: ColumnKind) -> &mut Column {
self.changes.push(Change::AddColumn(Column::new(name, kind)));
match self.changes.last_mut() {
Some(Change::AddColumn(column)) => column,
_ => unreachable!("just pushed an AddColumn"),
}
}
pub fn string(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Varchar(DEFAULT_STRING_LEN))
}
pub fn string_len(&mut self, name: impl Into<String>, len: u32) -> &mut Column {
self.column(name, ColumnKind::Varchar(len))
}
pub fn text(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Text))
}
pub fn integer(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Int))
}
pub fn big_integer(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::BigInt))
}
pub fn unsigned_big_integer(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::BigUint))
}
pub fn foreign_id(&mut self, name: impl Into<String>) -> &mut Column {
let column = self.column(name, ColumnKind::Basic(ColumnType::BigUint));
column.index();
column
}
pub fn boolean(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Bool))
}
pub fn timestamp(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Basic(ColumnType::Timestamp))
}
pub fn json(&mut self, name: impl Into<String>) -> &mut Column {
self.column(name, ColumnKind::Json)
}
pub fn drop_column(&mut self, name: impl Into<String>) -> &mut Self {
self.changes.push(Change::DropColumn(name.into()));
self
}
pub fn rename_column(&mut self, from: impl Into<String>, to: impl Into<String>) -> &mut Self {
self.changes.push(Change::RenameColumn(from.into(), to.into()));
self
}
pub fn index<I, S>(&mut self, columns: I) -> &mut IndexDef
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.push_index(columns, false, true)
}
pub fn unique<I, S>(&mut self, columns: I) -> &mut IndexDef
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.push_index(columns, true, true)
}
pub fn drop_index<I, S>(&mut self, columns: I) -> &mut IndexDef
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.push_index(columns, false, false)
}
pub fn drop_unique<I, S>(&mut self, columns: I) -> &mut IndexDef
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.push_index(columns, true, false)
}
fn push_index<I, S>(&mut self, columns: I, unique: bool, adding: bool) -> &mut IndexDef
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let columns: Vec<String> = columns.into_iter().map(Into::into).collect();
let suffix = if unique { "unique" } else { "index" };
let index =
IndexDef { name: conventional_name(&self.table, &columns, suffix), columns, unique };
self.changes.push(if adding { Change::AddIndex(index) } else { Change::DropIndex(index) });
match self.changes.last_mut() {
Some(Change::AddIndex(index) | Change::DropIndex(index)) => index,
_ => unreachable!("just pushed an index change"),
}
}
pub fn raw(
&mut self,
dialect: Option<Dialect>,
up: impl Into<String>,
down: Option<String>,
) -> &mut Self {
self.changes.push(Change::Raw(dialect, up.into(), down));
self
}
pub fn to_sql(&self, dialect: Dialect) -> Vec<String> {
let mut statements = Vec::new();
for change in &self.changes {
match change {
Change::AddColumn(column) => {
let mut stmt = Table::alter();
stmt.table(Alias::new(self.table.clone())).add_column(column.to_def());
statements.push(dialect.build_schema(&stmt));
if column.indexed {
statements.push(
dialect.build_schema(&self.column_index(column).to_stmt(&self.table)),
);
}
}
Change::DropColumn(name) => {
let mut stmt = Table::alter();
stmt.table(Alias::new(self.table.clone()))
.drop_column(Alias::new(name.clone()));
statements.push(dialect.build_schema(&stmt));
}
Change::RenameColumn(from, to) => {
statements.push(dialect.build_schema(&rename_column_stmt(
&self.table,
from,
to,
)));
}
Change::AddIndex(index) => {
statements.push(dialect.build_schema(&index.to_stmt(&self.table)));
}
Change::DropIndex(index) => {
statements.push(dialect.build_schema(&drop_index_stmt(&self.table, index)));
}
Change::Raw(only, sql, _) => {
if only.is_none() || *only == Some(dialect) {
statements.push(sql.clone());
}
}
}
}
statements
}
pub fn irreversible_because(&self) -> Option<String> {
for change in &self.changes {
match change {
Change::DropColumn(name) => {
return Some(format!(
"`{}`.`{name}` was dropped, and its type and data went with it",
self.table
))
}
Change::Raw(_, sql, None) => {
return Some(format!("a raw statement with no reverse: {sql}"))
}
_ => {}
}
}
None
}
pub fn to_reverse_sql(&self, dialect: Dialect) -> Vec<String> {
let mut statements = Vec::new();
for change in self.changes.iter().rev() {
match change {
Change::AddColumn(column) => {
if column.indexed {
statements.push(dialect.build_schema(&drop_index_stmt(
&self.table,
&self.column_index(column),
)));
}
let mut stmt = Table::alter();
stmt.table(Alias::new(self.table.clone()))
.drop_column(Alias::new(column.name.clone()));
statements.push(dialect.build_schema(&stmt));
}
Change::RenameColumn(from, to) => {
statements.push(dialect.build_schema(&rename_column_stmt(
&self.table,
to,
from,
)));
}
Change::AddIndex(index) => {
statements.push(dialect.build_schema(&drop_index_stmt(&self.table, index)));
}
Change::DropIndex(index) => {
statements.push(dialect.build_schema(&index.to_stmt(&self.table)));
}
Change::Raw(only, _, Some(down)) => {
if only.is_none() || *only == Some(dialect) {
statements.push(down.clone());
}
}
Change::DropColumn(_) | Change::Raw(_, _, None) => {}
}
}
statements
}
fn column_index(&self, column: &Column) -> IndexDef {
let columns = vec![column.name.clone()];
IndexDef { name: conventional_name(&self.table, &columns, "index"), columns, unique: false }
}
}
fn rename_column_stmt(table: &str, from: &str, to: &str) -> sea_query::TableAlterStatement {
let mut stmt = Table::alter();
stmt.table(Alias::new(table.to_string()))
.rename_column(Alias::new(from.to_string()), Alias::new(to.to_string()));
stmt
}
fn drop_index_stmt(table: &str, index: &IndexDef) -> sea_query::IndexDropStatement {
let mut stmt = Index::drop();
stmt.name(&index.name).table(Alias::new(table.to_string()));
stmt
}
fn strip_inline_primary(def: &mut ColumnDef, column: &Column) {
let mut without = column.clone();
without.primary = false;
without.auto_increment = false;
*def = without.to_def();
}
#[cfg(test)]
mod tests {
use super::*;
fn posts() -> Blueprint {
Blueprint::create("posts", |table| {
table.id();
table.string("slug").unique();
table.text("body");
table.boolean("published").default(false);
table.foreign_id("author_id").constrained_on("users").cascade_on_delete();
table.timestamps();
table.index(["published", "created_at"]);
})
}
#[test]
fn one_description_renders_for_every_engine() {
for dialect in [Dialect::Sqlite, Dialect::MySql, Dialect::Postgres] {
let sql = posts().to_sql(dialect).join("\n");
assert!(sql.contains("posts"), "{dialect:?}: {sql}");
assert!(sql.to_uppercase().contains("CREATE TABLE"), "{dialect:?}: {sql}");
assert!(sql.contains("author_id"), "{dialect:?}: {sql}");
}
}
#[test]
fn auto_increment_is_spelled_the_way_each_engine_spells_it() {
let sqlite = posts().to_sql(Dialect::Sqlite).join("\n");
let mysql = posts().to_sql(Dialect::MySql).join("\n");
let postgres = posts().to_sql(Dialect::Postgres).join("\n");
assert!(sqlite.contains("AUTOINCREMENT"), "{sqlite}");
assert!(mysql.contains("AUTO_INCREMENT"), "{mysql}");
assert!(postgres.contains("serial"), "{postgres}");
}
#[test]
fn a_key_never_renders_as_unsigned() {
let postgres = posts().to_sql(Dialect::Postgres).join("\n");
assert!(!postgres.to_lowercase().contains("unsigned"), "{postgres}");
}
#[test]
fn mysql_gets_no_if_not_exists_on_an_index_because_it_rejects_one() {
let mysql = posts().to_sql(Dialect::MySql).join("\n");
let index = mysql.lines().find(|line| line.contains("CREATE INDEX")).expect("an index");
assert!(!index.contains("IF NOT EXISTS"), "{index}");
let sqlite = posts().to_sql(Dialect::Sqlite).join("\n");
assert!(sqlite.contains("CREATE INDEX IF NOT EXISTS"), "{sqlite}");
}
#[test]
fn indexes_are_named_by_convention_so_a_rollback_can_find_them() {
let sql = posts().to_sql(Dialect::Sqlite).join("\n");
assert!(sql.contains("posts_published_created_at_index"), "{sql}");
assert!(sql.contains("posts_author_id_index"), "the foreign id is indexed: {sql}");
}
#[test]
fn a_column_index_and_a_table_index_agree_on_the_name() {
let by_column = Blueprint::create("t", |table| {
table.string("a").index();
});
let by_table = Blueprint::create("t", |table| {
table.string("a");
table.index(["a"]);
});
assert_eq!(by_column.to_sql(Dialect::Sqlite), by_table.to_sql(Dialect::Sqlite));
}
#[test]
fn columns_are_not_null_unless_they_say_otherwise() {
let sql = Blueprint::create("t", |table| {
table.string("required");
table.string("optional").nullable();
})
.to_sql(Dialect::Sqlite)
.join("\n");
assert!(sql.contains("\"required\" varchar(255) NOT NULL"), "{sql}");
assert!(sql.contains("\"optional\" varchar(255) NULL"), "{sql}");
}
#[test]
fn a_composite_primary_key_replaces_the_inline_one() {
let sql = Blueprint::create("post_tag", |table| {
table.foreign_id("post_id").constrained_on("posts").cascade_on_delete();
table.foreign_id("tag_id").constrained_on("tags").cascade_on_delete();
table.primary(["post_id", "tag_id"]);
})
.to_sql(Dialect::Sqlite)
.join("\n");
assert_eq!(sql.matches("PRIMARY KEY").count(), 1, "{sql}");
assert!(sql.contains("PRIMARY KEY (\"post_id\", \"tag_id\")"), "{sql}");
}
#[test]
fn foreign_keys_carry_their_action() {
let sql = posts().to_sql(Dialect::Sqlite).join("\n");
assert!(sql.contains("ON DELETE CASCADE"), "{sql}");
assert!(sql.contains("REFERENCES \"users\" (\"id\")"), "{sql}");
}
#[test]
fn the_reverse_of_a_create_is_a_drop() {
let sql = posts().to_reverse_sql(Dialect::Sqlite).join("\n");
assert_eq!(sql, "DROP TABLE IF EXISTS \"posts\"");
}
#[test]
fn json_becomes_each_engines_own_json_type() {
let blueprint = Blueprint::create("t", |table| {
table.json("payload");
});
let rendered = |d| blueprint.to_sql(d).join("\n").to_lowercase();
assert!(rendered(Dialect::Postgres).contains("jsonb"));
assert!(rendered(Dialect::MySql).contains("json"));
assert!(rendered(Dialect::Sqlite).contains("jsonb_text"));
}
#[test]
fn decimal_is_fixed_point_where_that_exists_and_a_float_where_it_does_not() {
let blueprint = Blueprint::create("t", |table| {
table.decimal("amount", 10, 2);
});
let rendered = |d| blueprint.to_sql(d).join("\n").to_lowercase();
assert!(rendered(Dialect::Postgres).contains("decimal(10, 2)"));
assert!(rendered(Dialect::MySql).contains("decimal(10, 2)"));
assert!(rendered(Dialect::Sqlite).contains("real(10, 2)"));
}
#[test]
fn binary_is_bytea_on_postgres_and_blob_elsewhere() {
let blueprint = Blueprint::create("t", |table| {
table.binary("bytes");
});
assert!(blueprint.to_sql(Dialect::Postgres).join("").contains("bytea"));
assert!(blueprint.to_sql(Dialect::MySql).join("").contains("blob"));
}
}