Commit 5bc3e961 authored by Julius de Bruijn's avatar Julius de Bruijn Committed by Katharina Fey
Browse files

Support for Microsoft SQL Server

parent 052b3f26
......@@ -34,6 +34,7 @@ default = []
diesel = ["tempfile", "diesel_rs"]
sqlite3 = []
mysql = []
mssql = []
pg = []
# Enables unstable (in-development) features,
......
edition = "2018"
......@@ -19,6 +19,11 @@ mod sqlite3;
#[cfg(feature = "sqlite3")]
pub use self::sqlite3::Sqlite;
#[cfg(feature = "mssql")]
mod mssql;
#[cfg(feature = "mssql")]
pub use self::mssql::MsSql;
#[allow(unused_imports)]
use crate::{types::Type, Migration};
......@@ -31,6 +36,8 @@ pub enum SqlVariant {
Pg,
#[cfg(feature = "mysql")]
Mysql,
#[cfg(feature = "mssql")]
Mssql,
#[doc(hidden)]
__Empty,
}
......@@ -47,6 +54,9 @@ impl SqlVariant {
#[cfg(feature = "mysql")]
SqlVariant::Mysql => _migr.make::<MySql>(),
#[cfg(feature = "mssql")]
SqlVariant::Mssql => _migr.make::<MsSql>(),
_ => panic!("You need to select an Sql variant!"),
}
}
......
//! Microsoft SQL Server implementation of a generator
//!
//! This module generates strings that are specific to SQL Server
//! databases. They should be thoroughly tested via unit testing
use super::SqlGenerator;
use crate::types::{BaseType, Type};
/// A simple macro that will generate a quoted schema prefix if it exists
macro_rules! quoted_prefix {
($schema:expr) => {
$schema
.map(|s| format!("[{}].", s))
.unwrap_or_else(|| String::new())
};
}
/// A simple macro that will generate a schema prefix if it exists
macro_rules! prefix {
($schema:expr) => {
$schema
.map(|s| format!("{}.", s))
.unwrap_or_else(|| String::new())
};
}
/// SQL Server generator backend
pub struct MsSql;
impl SqlGenerator for MsSql {
fn create_table(name: &str, schema: Option<&str>) -> String {
format!("CREATE TABLE {}[{}]", quoted_prefix!(schema), name)
}
fn create_table_if_not_exists(name: &str, schema: Option<&str>) -> String {
let table = format!("{}[{}]", quoted_prefix!(schema), name);
match schema {
None => {
format!(
"IF NOT EXISTS (SELECT * FROM sys.tables WHERE name='{table}') CREATE TABLE {quoted}",
table = name,
quoted = table,
)
}
Some(schema) => {
format!(
"IF NOT EXISTS (SELECT * FROM sys.tables WHERE name='{table}' AND SCHEMA_NAME(schema_id) = '{schema}') CREATE TABLE {quoted}",
table = name,
quoted = table,
schema = schema,
)
}
}
}
fn drop_table(name: &str, schema: Option<&str>) -> String {
format!("DROP TABLE {}[{}]", quoted_prefix!(schema), name)
}
fn drop_table_if_exists(name: &str, schema: Option<&str>) -> String {
format!("DROP TABLE IF EXISTS {}[{}]", quoted_prefix!(schema), name)
}
fn rename_table(old: &str, new: &str, schema: Option<&str>) -> String {
let prefix = prefix!(schema);
format!(r#"EXEC sp_rename '{}{}', '{}'"#, prefix, old, new)
}
fn alter_table(name: &str, schema: Option<&str>) -> String {
format!("ALTER TABLE {}[{}]", quoted_prefix!(schema), name)
}
fn add_column(ex: bool, schema: Option<&str>, name: &str, tt: &Type) -> String {
let bt: BaseType = tt.get_inner();
let name_and_type = match bt {
BaseType::Array(_) => panic!("Arrays are not supported with SQL Server"),
BaseType::Index(_) => unreachable!("Indices are handled via custom builder"),
_ => format!(
"{}[{}] {}",
MsSql::prefix(ex),
name,
MsSql::print_type(bt, schema)
),
};
let primary_key_indicator = match tt.primary {
true => " PRIMARY KEY",
false => "",
};
let default_indicator = match (&tt.default).as_ref() {
Some(ref m) => format!(" DEFAULT '{}'", m),
_ => format!(""),
};
let nullable_indicator = match tt.nullable {
true => "",
false => " NOT NULL",
};
let unique_indicator = match tt.unique {
true => " UNIQUE",
false => "",
};
format!(
"{}{}{}{}{}",
// `ADD name VARCHAR(max)` or `name VARCHAR(max)`
name_and_type,
// `PRIMARY KEY` or nothing
primary_key_indicator,
// `DEFAULT 'foo'` or nothing
default_indicator,
// `NOT NULL` or nothing
nullable_indicator,
// `UNIQUE or nothing`
unique_indicator
)
}
fn drop_column(name: &str) -> String {
format!("DROP COLUMN [{}]", name)
}
fn rename_column(old: &str, new: &str) -> String {
format!(r#"EXEC sp_rename '{}', '{}'"#, old, new)
}
fn create_index(table: &str, schema: Option<&str>, name: &str, _type: &Type) -> String {
// FIXME: Implement PG specific index builder here
format!(
"CREATE {} INDEX [{}] ON {}[{}] ({})",
match _type.unique {
true => "UNIQUE",
false => "",
},
name,
prefix!(schema),
table,
match _type.inner {
BaseType::Index(ref cols) => cols
.iter()
.map(|col| format!("[{}]", col))
.collect::<Vec<_>>()
.join(", "),
_ => unreachable!(),
}
)
}
fn drop_index(name: &str) -> String {
format!("DROP INDEX [{}]", name)
}
}
impl MsSql {
fn prefix(ex: bool) -> String {
match ex {
true => format!("ADD "),
false => format!(""),
}
}
fn print_type(t: BaseType, schema: Option<&str>) -> String {
use self::BaseType::*;
match t {
Text => format!("TEXT"),
Varchar(l) => match l {
0 => format!("VARCHAR(MAX)"), // For "0" remove the limit
_ => format!("VARCHAR({})", l),
},
/* "NOT NULL" is added here because normally primary keys are implicitly not-null */
Primary => format!("IDENTITY(1,1) PRIMARY KEY NOT NULL"),
Integer => format!("INT"),
Float => format!("FLOAT(24)"),
Double => format!("FLOAT(53)"),
UUID => format!("UNIQUEIDENTIFIER"),
Boolean => format!("BIT"),
Date => format!("DATE"),
Json => format!("JSON"),
Binary => format!("VARBINARY(MAX)"),
Foreign(s, t, refs) => format!(
"INT REFERENCES {}[{}]({})",
quoted_prefix!(s.or(schema.map(|s| s.into()))),
t,
refs.0
.iter()
.map(|r| format!("[{}]", r))
.collect::<Vec<_>>()
.join(",")
),
Custom(t) => format!("{}", t),
Array(meh) => format!("{}[]", MsSql::print_type(*meh, schema)),
Index(_) => unreachable!("Indices are handled via custom builder"),
}
}
}
......@@ -66,7 +66,7 @@ impl SqlGenerator for MySql {
Foreign(_, _, _) => format!("{}{} {}", MySql::prefix(ex), name, MySql::print_type(bt, schema)),
Custom(_) => format!("{}{} {}", MySql::prefix(ex), name, MySql::print_type(bt, schema)),
Array(it) => format!("{}{} {}", MySql::prefix(ex), name, MySql::print_type(Array(Box::new(*it)), schema)),
Index(_) => unreachable!(),
Index(_) => unreachable!("Indices are handled via custom builder"),
},
match tt.primary {
true => " PRIMARY KEY",
......
......@@ -68,7 +68,7 @@ impl SqlGenerator for Pg {
Foreign(_, _, _) => format!("{}\"{}\" {}", Pg::prefix(ex), name, Pg::print_type(bt, schema)),
Custom(_) => format!("{}\"{}\" {}", Pg::prefix(ex), name, Pg::print_type(bt, schema)),
Array(it) => format!("{}\"{}\" {}", Pg::prefix(ex), name, Pg::print_type(Array(Box::new(*it)), schema)),
Index(_) => unreachable!(), // Indices are handled via custom builder
Index(_) => unreachable!("Indices are handled via custom builder"),
},
match tt.primary {
true => " PRIMARY KEY",
......@@ -158,7 +158,7 @@ impl Pg {
),
Custom(t) => format!("{}", t),
Array(meh) => format!("{}[]", Pg::print_type(*meh, schema)),
Index(_) => unreachable!(), // Indices are handled via custom builder
Index(_) => unimplemented!("Indices are handled via custom builder"),
}
}
}
......@@ -64,7 +64,7 @@ impl SqlGenerator for Sqlite {
Foreign(_, _, _) => format!("{}\"{}\" {}", Sqlite::prefix(ex), name, Sqlite::print_type(bt)),
Custom(_) => format!("{}\"{}\" {}", Sqlite::prefix(ex), name, Sqlite::print_type(bt)),
Array(it) => format!("{}\"{}\" {}", Sqlite::prefix(ex), name, Sqlite::print_type(Array(Box::new(*it)))),
Index(_) => unreachable!(), // Indices are handled via custom builders
Index(_) => unreachable!("Indices are handled via custom builder"),
},
match tt.primary {
true => " PRIMARY KEY",
......
......@@ -156,7 +156,7 @@ pub enum DatabaseChange {
/// Only drop a table if it exists
DropTableIfExists(String),
/// Add some custom SQL if all else fails
CustomLine(String),
}
......
......@@ -132,7 +132,6 @@ impl Migration {
variant.run_for(self)
}
/// Inject a line of custom SQL into the top-level migration scope
///
/// This is a bypass to the barrel typesystem, in case there is
......@@ -144,7 +143,7 @@ impl Migration {
pub fn inject_custom<S: Into<String>>(&mut self, sql: S) {
self.changes.push(DatabaseChange::CustomLine(sql.into()));
}
/// Automatically infer the `down` step of this migration
///
/// Will thrown an error if behaviour is ambiguous or not
......
......@@ -11,3 +11,6 @@ mod pg;
#[cfg(feature = "sqlite3")]
mod sqlite3;
#[cfg(feature = "mssql")]
mod mssql;
//! All add_column combinations for pgsql
#![allow(unused_imports)]
use crate::backend::{MsSql, SqlGenerator};
use crate::types;
#[test]
fn text() {
let sql = MsSql::add_column(true, None, "Text", &types::text());
assert_eq!(String::from("ADD [Text] TEXT NOT NULL"), sql);
}
#[test]
fn varchar() {
let sql = MsSql::add_column(true, None, "Varchar", &types::varchar(255));
assert_eq!(String::from("ADD [Varchar] VARCHAR(255) NOT NULL"), sql);
}
#[test]
fn integer() {
let sql = MsSql::add_column(true, None, "Integer", &types::integer());
assert_eq!(String::from("ADD [Integer] INT NOT NULL"), sql);
}
#[test]
fn float() {
let sql = MsSql::add_column(true, None, "Float", &types::float());
assert_eq!(String::from("ADD [Float] FLOAT(24) NOT NULL"), sql);
}
#[test]
fn double() {
let sql = MsSql::add_column(true, None, "Double", &types::double());
assert_eq!(String::from("ADD [Double] FLOAT(53) NOT NULL"), sql);
}
#[test]
fn boolean() {
let sql = MsSql::add_column(true, None, "Boolean", &types::boolean());
assert_eq!(String::from("ADD [Boolean] BIT NOT NULL"), sql);
}
#[test]
fn binary() {
let sql = MsSql::add_column(true, None, "Binary", &types::binary());
assert_eq!(String::from("ADD [Binary] VARBINARY(MAX) NOT NULL"), sql);
}
#[test]
fn date() {
let sql = MsSql::add_column(true, None, "Date", &types::date());
assert_eq!(String::from("ADD [Date] DATE NOT NULL"), sql);
}
#[test]
fn foreign() {
let sql = MsSql::add_column(true, None, "Foreign", &types::foreign("posts", "id"));
assert_eq!(
String::from("ADD [Foreign] INT REFERENCES [posts]([id]) NOT NULL"),
sql
);
}
#[test]
fn custom() {
let sql = MsSql::add_column(true, None, "Xml", &types::custom("XML"));
assert_eq!(String::from("ADD [Xml] XML NOT NULL"), sql);
}
//! Some unit tests that create create tables
#![allow(unused_imports)]
use crate::backend::{MsSql, SqlGenerator};
use crate::{types, Migration, Table};
#[test]
fn create_table_if_not_exists_doesnt_hit_unreachable() {
let mut m = Migration::new();
m.create_table_if_not_exists("artist", |t| {
t.add_column("id", types::primary());
t.add_column("name", types::text().nullable(true));
t.add_column("description", types::text().nullable(true));
t.add_column("pic", types::text().nullable(true));
t.add_column("mbid", types::text().nullable(true));
});
assert_eq!(m.make::<MsSql>(), String::from("IF NOT EXISTS (SELECT * FROM sys.tables WHERE name='artist') CREATE TABLE [artist] ([id] IDENTITY(1,1) PRIMARY KEY NOT NULL, [name] TEXT, [description] TEXT, [pic] TEXT, [mbid] TEXT);"));
}
#[test]
fn basic_fields() {
let mut m = Migration::new();
m.create_table("users", |t: &mut Table| {
t.add_column("id", types::primary());
t.add_column("name", types::varchar(255));
t.add_column("age", types::integer());
t.add_column("plushy_sharks_owned", types::boolean());
});
assert_eq!(
m.make::<MsSql>(),
String::from("CREATE TABLE [users] ([id] IDENTITY(1,1) PRIMARY KEY NOT NULL, [name] VARCHAR(255) NOT NULL, [age] INT NOT NULL, [plushy_sharks_owned] BIT NOT NULL);")
);
}
#[test]
fn basic_fields_nullable() {
let mut m = Migration::new();
m.create_table("users", |t: &mut Table| {
t.add_column("id", types::primary());
t.add_column("name", types::varchar(255).nullable(true));
t.add_column("age", types::integer().nullable(true));
t.add_column("plushy_sharks_owned", types::boolean().nullable(true));
});
assert_eq!(
m.make::<MsSql>(),
String::from("CREATE TABLE [users] ([id] IDENTITY(1,1) PRIMARY KEY NOT NULL, [name] VARCHAR(255), [age] INT, [plushy_sharks_owned] BIT);")
);
}
#[test]
fn create_multiple_tables() {
let mut m = Migration::new();
m.create_table("artist", |t| {
t.add_column("id", types::primary());
t.add_column("name", types::text());
t.add_column("description", types::text());
t.add_column("pic", types::text());
t.add_column("mbid", types::text());
});
m.create_table("album", |t| {
t.add_column("id", types::primary());
t.add_column("name", types::text());
t.add_column("pic", types::text());
t.add_column("mbid", types::text());
});
assert_eq!(m.make::<MsSql>(), String::from("CREATE TABLE [artist] ([id] IDENTITY(1,1) PRIMARY KEY NOT NULL, [name] TEXT NOT NULL, [description] TEXT NOT NULL, [pic] TEXT NOT NULL, [mbid] TEXT NOT NULL);CREATE TABLE [album] ([id] IDENTITY(1,1) PRIMARY KEY NOT NULL, [name] TEXT NOT NULL, [pic] TEXT NOT NULL, [mbid] TEXT NOT NULL);"));
}
#[test]
fn drop_table() {
let mut m = Migration::new();
m.drop_table("users");
assert_eq!(m.make::<MsSql>(), String::from("DROP TABLE [users];"));
}
#[test]
fn drop_table_if_exists() {
let mut m = Migration::new();
m.drop_table_if_exists("users");
assert_eq!(
m.make::<MsSql>(),
String::from("DROP TABLE IF EXISTS [users];")
);
}
#[test]
fn rename_table() {
let mut m = Migration::new();
m.rename_table("users", "cool_users");
assert_eq!(
m.make::<MsSql>(),
String::from("EXEC sp_rename 'users', 'cool_users';")
);
}
//! Test pgsql generation
mod add_column;
mod create_table;
mod reference;
mod simple;
#![allow(unused_imports)]
use crate::backend::{MsSql, SqlGenerator};
use crate::{types, Migration, Table};
#[test]
fn in_schema() {
let sql = MsSql::add_column(
false,
Some("schema"),
"author",
&types::foreign("users", "id"),
);
assert_eq!(
sql,
"[author] INT REFERENCES [schema].[users]([id]) NOT NULL"
);
}
#[test]
fn ext_schema() {
let sql = MsSql::add_column(
false,
Some("schema"),
"author",
&types::foreign_schema("other_schema", "users", "id"),
);
assert_eq!(
sql,
"[author] INT REFERENCES [other_schema].[users]([id]) NOT NULL"
);
}
//! Other simple table/ column migrations
#![allow(unused_imports)]
use crate::backend::{MsSql, SqlGenerator};
#[test]
fn create_table() {
let sql = MsSql::create_table("table_to_create", None);
assert_eq!(String::from("CREATE TABLE [table_to_create]"), sql);
}
#[test]
fn create_table_with_schema() {
let sql = MsSql::create_table("table_to_create", Some("my_schema"));
assert_eq!(
String::from("CREATE TABLE [my_schema].[table_to_create]"),
sql
);
}
#[test]
fn create_table_if_not_exists() {
let sql = MsSql::create_table_if_not_exists("table_to_create", None);
assert_eq!(String::from("IF NOT EXISTS (SELECT * FROM sys.tables WHERE name=\'table_to_create\') CREATE TABLE [table_to_create]"), sql);
}
#[test]
fn drop_table() {
let sql = MsSql::drop_table("table_to_drop", None);
assert_eq!(String::from("DROP TABLE [table_to_drop]"), sql);
}
#[test]
fn drop_table_if_exists() {
let sql = MsSql::drop_table_if_exists("table_to_drop", None);
assert_eq!(String::from("DROP TABLE IF EXISTS [table_to_drop]"), sql);
}
#[test]
fn rename_table() {
let sql = MsSql::rename_table("old_table", "new_table", None);
assert_eq!(String::from("EXEC sp_rename 'old_table', 'new_table'"), sql);
}
#[test]
fn alter_table() {
let sql = MsSql::alter_table("table_to_alter", None);
assert_eq!(String::from("ALTER TABLE [table_to_alter]"), sql);
}
#[test]
fn drop_column() {
let sql = MsSql::drop_column("column_to_drop");
assert_eq!(String::from("DROP COLUMN [column_to_drop]"), sql);
}
#[test]
fn rename_column() {
let sql = MsSql::rename_column("table.old_column", "table.new_column");
assert_eq!(
String::from("EXEC sp_rename 'table.old_column', 'table.new_column'"),
sql
);
}
......@@ -13,7 +13,10 @@ fn create_table() {
#[test]
fn create_table_with_schema() {
let sql = MySql::create_table("table_to_create", Some("my_schema"));
assert_eq!(String::from("CREATE TABLE `my_schema`.`table_to_create`"), sql);
assert_eq!(
String::from("CREATE TABLE `my_schema`.`table_to_create`"),
sql
);
}
#[test]
......
......@@ -98,7 +98,12 @@ fn array_varchar() {
#[test]
fn array_integer() {
let sql = Pg::add_column(true, None, "Array of Integer", &types::array(&types::integer()));