pub mod access;
pub mod mysql;
pub mod postgres;
pub mod sqlite;
pub use access::Access;
pub use mysql::Mysql;
pub use postgres::Postgres;
pub use sqlite::Sqlite;
use crate::format::{column_flags, index_flags, index_type};
use crate::{ColumnDef, IndexDef, Relationship, TableDef};
pub trait DdlDialect {
fn quote_id(&self, name: &str) -> String;
fn map_column_type(&self, col: &ColumnDef, is_auto: bool) -> String;
fn auto_increment_absorbs_pk(&self) -> bool;
fn inline_foreign_keys(&self) -> bool;
}
fn find_primary_key(tdef: &TableDef) -> Option<&IndexDef> {
tdef.indexes.iter().find(|idx| {
idx.index_type != index_type::FOREIGN_KEY
&& (idx.flags & index_flags::UNIQUE) != 0
&& (idx.flags & index_flags::REQUIRED) != 0
})
}
fn is_auto_increment(col: &ColumnDef) -> bool {
(col.flags & column_flags::AUTO_LONG) != 0 || (col.flags & column_flags::AUTO_UUID) != 0
}
fn resolve_col_name(tdef: &TableDef, col_num: u16) -> &str {
tdef.columns
.iter()
.find(|c| c.col_num == col_num)
.map(|c| c.name.as_str())
.unwrap_or("?")
}
pub fn generate_ddl(
dialect: &dyn DdlDialect,
tables: &[TableDef],
relationships: &[Relationship],
include_indexes: bool,
include_relations: bool,
) -> String {
let mut out = String::new();
for (i, tdef) in tables.iter().enumerate() {
if i > 0 {
out.push('\n');
}
let table_rels: Vec<&Relationship> = if dialect.inline_foreign_keys() && include_relations {
relationships
.iter()
.filter(|r| r.from_table == tdef.name)
.collect()
} else {
Vec::new()
};
out.push_str(&generate_create_table(dialect, tdef, &table_rels));
}
if include_indexes {
for tdef in tables {
let idx_sql = generate_create_indexes(dialect, tdef);
if !idx_sql.is_empty() {
out.push('\n');
out.push_str(&idx_sql);
}
}
}
if include_relations && !dialect.inline_foreign_keys() {
let table_names: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect();
let filtered_rels: Vec<&Relationship> = relationships
.iter()
.filter(|r| table_names.contains(&r.from_table.as_str()))
.collect();
let fk_sql = generate_foreign_keys(dialect, &filtered_rels);
if !fk_sql.is_empty() {
out.push('\n');
out.push_str(&fk_sql);
}
}
out
}
pub fn generate_create_table(
dialect: &dyn DdlDialect,
tdef: &TableDef,
table_rels: &[&Relationship],
) -> String {
let pk = find_primary_key(tdef);
let pk_col_nums: Vec<u16> = pk
.map(|idx| idx.columns.iter().map(|c| c.col_num).collect())
.unwrap_or_default();
let auto_pk_col = pk.and_then(|_| {
tdef.columns
.iter()
.find(|c| pk_col_nums.contains(&c.col_num) && is_auto_increment(c))
});
let suppress_pk = dialect.auto_increment_absorbs_pk() && auto_pk_col.is_some();
let mut lines: Vec<String> = Vec::new();
for col in &tdef.columns {
let is_auto = is_auto_increment(col);
let type_str = dialect.map_column_type(col, is_auto);
let not_null = if (col.flags & column_flags::NULLABLE) == 0 {
" NOT NULL"
} else {
""
};
if is_auto {
lines.push(format!(" {} {}", dialect.quote_id(&col.name), type_str));
} else {
lines.push(format!(
" {} {}{}",
dialect.quote_id(&col.name),
type_str,
not_null
));
}
}
if !suppress_pk {
if let Some(pk_idx) = pk {
let pk_cols: Vec<String> = pk_idx
.columns
.iter()
.map(|c| dialect.quote_id(resolve_col_name(tdef, c.col_num)))
.collect();
lines.push(format!(" PRIMARY KEY ({})", pk_cols.join(", ")));
}
}
for rel in table_rels {
let from_cols: Vec<String> = rel
.columns
.iter()
.map(|c| dialect.quote_id(&c.from_column))
.collect();
let to_cols: Vec<String> = rel
.columns
.iter()
.map(|c| dialect.quote_id(&c.to_column))
.collect();
let mut fk = format!(
" FOREIGN KEY ({}) REFERENCES {} ({})",
from_cols.join(", "),
dialect.quote_id(&rel.to_table),
to_cols.join(", ")
);
append_cascade_clauses(&mut fk, rel);
lines.push(fk);
}
format!(
"CREATE TABLE {} (\n{}\n);\n",
dialect.quote_id(&tdef.name),
lines.join(",\n")
)
}
pub fn generate_create_indexes(dialect: &dyn DdlDialect, tdef: &TableDef) -> String {
let pk = find_primary_key(tdef);
let mut out = String::new();
for idx in &tdef.indexes {
if idx.index_type == index_type::FOREIGN_KEY {
continue;
}
if let Some(pk_idx) = pk {
if idx.index_num == pk_idx.index_num {
continue;
}
}
let unique = if (idx.flags & index_flags::UNIQUE) != 0 {
"UNIQUE "
} else {
""
};
let cols: Vec<String> = idx
.columns
.iter()
.map(|c| dialect.quote_id(resolve_col_name(tdef, c.col_num)))
.collect();
out.push_str(&format!(
"CREATE {unique}INDEX {} ON {} ({});\n",
dialect.quote_id(&idx.name),
dialect.quote_id(&tdef.name),
cols.join(", ")
));
}
out
}
pub fn generate_foreign_keys(dialect: &dyn DdlDialect, relationships: &[&Relationship]) -> String {
let mut out = String::new();
for rel in relationships {
let from_cols: Vec<String> = rel
.columns
.iter()
.map(|c| dialect.quote_id(&c.from_column))
.collect();
let to_cols: Vec<String> = rel
.columns
.iter()
.map(|c| dialect.quote_id(&c.to_column))
.collect();
let mut stmt = format!(
"ALTER TABLE {} ADD CONSTRAINT {}\n FOREIGN KEY ({}) REFERENCES {} ({})",
dialect.quote_id(&rel.from_table),
dialect.quote_id(&rel.name),
from_cols.join(", "),
dialect.quote_id(&rel.to_table),
to_cols.join(", ")
);
append_cascade_clauses(&mut stmt, rel);
stmt.push_str(";\n");
out.push_str(&stmt);
}
out
}
fn append_cascade_clauses(stmt: &mut String, rel: &Relationship) {
use crate::relationship::relationship_flags;
if (rel.flags & relationship_flags::CASCADE_UPDATE) != 0 {
stmt.push_str("\n ON UPDATE CASCADE");
}
if (rel.flags & relationship_flags::CASCADE_DELETE) != 0 {
stmt.push_str("\n ON DELETE CASCADE");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::{column_flags, index_flags, index_type, ColumnType};
use crate::{
ColumnDef, IndexColumn, IndexColumnOrder, IndexDef, Relationship, RelationshipColumn,
TableDef,
};
fn col(
name: &str,
col_type: ColumnType,
col_size: u16,
flags: u8,
precision: u8,
scale: u8,
) -> ColumnDef {
ColumnDef {
name: name.to_string(),
col_type,
col_num: 0,
var_col_num: 0,
fixed_offset: 0,
col_size,
flags,
is_fixed: false,
precision,
scale,
}
}
fn col_with_num(
name: &str,
col_type: ColumnType,
col_size: u16,
flags: u8,
precision: u8,
scale: u8,
col_num: u16,
) -> ColumnDef {
ColumnDef {
name: name.to_string(),
col_type,
col_num,
var_col_num: 0,
fixed_offset: 0,
col_size,
flags,
is_fixed: false,
precision,
scale,
}
}
fn index(name: &str, col_nums: &[u16], flags: u8, idx_type: u8, index_num: u16) -> IndexDef {
IndexDef {
name: name.to_string(),
index_num,
index_type: idx_type,
columns: col_nums
.iter()
.map(|&n| IndexColumn {
col_num: n,
order: IndexColumnOrder::Ascending,
})
.collect(),
flags,
first_data_page: 0,
foreign_key: None,
}
}
fn table(name: &str, columns: Vec<ColumnDef>, indexes: Vec<IndexDef>) -> TableDef {
TableDef {
name: name.to_string(),
num_rows: 0,
num_cols: columns.len() as u16,
num_var_cols: 0,
columns,
indexes,
data_pages: vec![],
}
}
fn relationship(
name: &str,
from_table: &str,
to_table: &str,
col_pairs: &[(&str, &str)],
flags: u32,
) -> Relationship {
Relationship {
name: name.to_string(),
from_table: from_table.to_string(),
to_table: to_table.to_string(),
columns: col_pairs
.iter()
.map(|(f, t)| RelationshipColumn {
from_column: f.to_string(),
to_column: t.to_string(),
})
.collect(),
flags,
}
}
fn sqlite() -> Box<dyn DdlDialect> {
Box::new(Sqlite)
}
fn postgres() -> Box<dyn DdlDialect> {
Box::new(Postgres)
}
fn mysql() -> Box<dyn DdlDialect> {
Box::new(Mysql)
}
fn access() -> Box<dyn DdlDialect> {
Box::new(Access)
}
#[test]
fn sqlite_map_text() {
let d = sqlite();
let c = col("x", ColumnType::Text, 100, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "TEXT");
}
#[test]
fn sqlite_map_long() {
let d = sqlite();
let c = col("x", ColumnType::Long, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "INTEGER");
}
#[test]
fn sqlite_map_long_auto() {
let d = sqlite();
let c = col("x", ColumnType::Long, 0, column_flags::AUTO_LONG, 0, 0);
assert_eq!(
d.map_column_type(&c, true),
"INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT"
);
}
#[test]
fn sqlite_map_money() {
let d = sqlite();
let c = col("x", ColumnType::Money, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "NUMERIC");
}
#[test]
fn sqlite_map_timestamp() {
let d = sqlite();
let c = col("x", ColumnType::Timestamp, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "TEXT");
}
#[test]
fn sqlite_map_binary() {
let d = sqlite();
let c = col("x", ColumnType::Binary, 50, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BLOB");
}
#[test]
fn sqlite_map_guid() {
let d = sqlite();
let c = col("x", ColumnType::Guid, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "TEXT");
}
#[test]
fn postgres_map_text() {
let d = postgres();
let c = col("x", ColumnType::Text, 100, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "VARCHAR(100)");
}
#[test]
fn postgres_map_boolean() {
let d = postgres();
let c = col("x", ColumnType::Boolean, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BOOLEAN");
}
#[test]
fn postgres_map_guid() {
let d = postgres();
let c = col("x", ColumnType::Guid, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "UUID");
}
#[test]
fn postgres_map_long_auto() {
let d = postgres();
let c = col("x", ColumnType::Long, 0, column_flags::AUTO_LONG, 0, 0);
assert_eq!(
d.map_column_type(&c, true),
"INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY"
);
}
#[test]
fn postgres_map_timestamp() {
let d = postgres();
let c = col("x", ColumnType::Timestamp, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "TIMESTAMP WITHOUT TIME ZONE");
}
#[test]
fn postgres_map_money() {
let d = postgres();
let c = col("x", ColumnType::Money, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "NUMERIC(19,4)");
}
#[test]
fn postgres_map_ole() {
let d = postgres();
let c = col("x", ColumnType::Ole, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BYTEA");
}
#[test]
fn mysql_map_text() {
let d = mysql();
let c = col("x", ColumnType::Text, 100, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "VARCHAR(100)");
}
#[test]
fn mysql_map_boolean() {
let d = mysql();
let c = col("x", ColumnType::Boolean, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BOOLEAN");
}
#[test]
fn mysql_map_byte() {
let d = mysql();
let c = col("x", ColumnType::Byte, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "TINYINT UNSIGNED");
}
#[test]
fn mysql_map_long_auto() {
let d = mysql();
let c = col("x", ColumnType::Long, 0, column_flags::AUTO_LONG, 0, 0);
assert_eq!(d.map_column_type(&c, true), "INT NOT NULL AUTO_INCREMENT");
}
#[test]
fn mysql_map_ole() {
let d = mysql();
let c = col("x", ColumnType::Ole, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "LONGBLOB");
}
#[test]
fn mysql_map_guid() {
let d = mysql();
let c = col("x", ColumnType::Guid, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "CHAR(36)");
}
#[test]
fn access_map_long_auto() {
let d = access();
let c = col("x", ColumnType::Long, 0, column_flags::AUTO_LONG, 0, 0);
assert_eq!(d.map_column_type(&c, true), "COUNTER NOT NULL");
}
#[test]
fn access_map_money() {
let d = access();
let c = col("x", ColumnType::Money, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "CURRENCY");
}
#[test]
fn access_map_boolean() {
let d = access();
let c = col("x", ColumnType::Boolean, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "YESNO");
}
#[test]
fn access_map_text() {
let d = access();
let c = col("x", ColumnType::Text, 100, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "TEXT(100)");
}
#[test]
fn access_map_memo() {
let d = access();
let c = col("x", ColumnType::Memo, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "MEMO");
}
#[test]
fn sqlite_quote_id() {
let d = sqlite();
assert_eq!(d.quote_id("Table 1"), "\"Table 1\"");
}
#[test]
fn sqlite_quote_id_escape() {
let d = sqlite();
assert_eq!(d.quote_id("col\"x"), "\"col\"\"x\"");
}
#[test]
fn postgres_quote_id_escape() {
let d = postgres();
assert_eq!(d.quote_id("col\"x"), "\"col\"\"x\"");
}
#[test]
fn mysql_quote_id() {
let d = mysql();
assert_eq!(d.quote_id("Table 1"), "`Table 1`");
}
#[test]
fn mysql_quote_id_escape() {
let d = mysql();
assert_eq!(d.quote_id("col`x"), "`col``x`");
}
#[test]
fn access_quote_id() {
let d = access();
assert_eq!(d.quote_id("Table 1"), "[Table 1]");
}
#[test]
fn access_quote_id_escape() {
let d = access();
assert_eq!(d.quote_id("col]x"), "[col]]x]");
}
#[test]
fn create_table_basic_postgres() {
let d = postgres();
let tdef = table(
"T",
vec![
col_with_num("A", ColumnType::Text, 100, 0, 0, 0, 1),
col_with_num("B", ColumnType::Long, 0, column_flags::NULLABLE, 0, 0, 2),
],
vec![],
);
let result = generate_create_table(&*d, &tdef, &[]);
assert_eq!(
result,
"CREATE TABLE \"T\" (\n \"A\" VARCHAR(100) NOT NULL,\n \"B\" INTEGER\n);\n"
);
}
#[test]
fn create_table_pk_postgres() {
let d = postgres();
let tdef = table(
"T",
vec![col_with_num("id", ColumnType::Long, 0, 0, 0, 0, 1)],
vec![index(
"PrimaryKey",
&[1],
index_flags::UNIQUE | index_flags::REQUIRED,
index_type::NORMAL,
0,
)],
);
let result = generate_create_table(&*d, &tdef, &[]);
assert!(result.contains("PRIMARY KEY (\"id\")"), "got:\n{result}");
}
#[test]
fn create_table_sqlite_auto_pk() {
let d = sqlite();
let tdef = table(
"T",
vec![col_with_num(
"id",
ColumnType::Long,
0,
column_flags::FIXED | column_flags::AUTO_LONG,
0,
0,
1,
)],
vec![index(
"PrimaryKey",
&[1],
index_flags::UNIQUE | index_flags::REQUIRED,
index_type::NORMAL,
0,
)],
);
let result = generate_create_table(&*d, &tdef, &[]);
assert!(
result.contains("INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT"),
"got:\n{result}"
);
assert!(
!result.contains(" PRIMARY KEY"),
"should not have table-level PK, got:\n{result}"
);
}
#[test]
fn create_table_mysql_auto_increment() {
let d = mysql();
let tdef = table(
"T",
vec![col_with_num(
"id",
ColumnType::Long,
0,
column_flags::FIXED | column_flags::AUTO_LONG,
0,
0,
1,
)],
vec![index(
"PrimaryKey",
&[1],
index_flags::UNIQUE | index_flags::REQUIRED,
index_type::NORMAL,
0,
)],
);
let result = generate_create_table(&*d, &tdef, &[]);
assert!(
result.contains("INT NOT NULL AUTO_INCREMENT"),
"got:\n{result}"
);
assert!(result.contains("PRIMARY KEY (`id`)"), "got:\n{result}");
}
#[test]
fn create_table_access_counter() {
let d = access();
let tdef = table(
"T",
vec![col_with_num(
"id",
ColumnType::Long,
0,
column_flags::FIXED | column_flags::AUTO_LONG,
0,
0,
1,
)],
vec![index(
"PrimaryKey",
&[1],
index_flags::UNIQUE | index_flags::REQUIRED,
index_type::NORMAL,
0,
)],
);
let result = generate_create_table(&*d, &tdef, &[]);
assert!(result.contains("COUNTER NOT NULL"), "got:\n{result}");
assert!(result.contains("PRIMARY KEY ([id])"), "got:\n{result}");
}
#[test]
fn create_index_basic() {
let d = postgres();
let tdef = table(
"T",
vec![col_with_num("B", ColumnType::Long, 0, 0, 0, 0, 2)],
vec![index("idx_B", &[2], 0, index_type::NORMAL, 1)],
);
let result = generate_create_indexes(&*d, &tdef);
assert_eq!(result, "CREATE INDEX \"idx_B\" ON \"T\" (\"B\");\n");
}
#[test]
fn create_index_unique() {
let d = postgres();
let tdef = table(
"T",
vec![col_with_num("B", ColumnType::Long, 0, 0, 0, 0, 2)],
vec![index(
"idx_B",
&[2],
index_flags::UNIQUE,
index_type::NORMAL,
1,
)],
);
let result = generate_create_indexes(&*d, &tdef);
assert_eq!(result, "CREATE UNIQUE INDEX \"idx_B\" ON \"T\" (\"B\");\n");
}
#[test]
fn create_index_skip_pk() {
let d = postgres();
let tdef = table(
"T",
vec![col_with_num("id", ColumnType::Long, 0, 0, 0, 0, 1)],
vec![index(
"PrimaryKey",
&[1],
index_flags::UNIQUE | index_flags::REQUIRED,
index_type::NORMAL,
0,
)],
);
let result = generate_create_indexes(&*d, &tdef);
assert_eq!(result, "", "PK index should be skipped");
}
#[test]
fn create_index_skip_fk() {
let d = postgres();
let tdef = table(
"T",
vec![col_with_num("fk_id", ColumnType::Long, 0, 0, 0, 0, 1)],
vec![index("fk_idx", &[1], 0, index_type::FOREIGN_KEY, 0)],
);
let result = generate_create_indexes(&*d, &tdef);
assert_eq!(result, "", "FK index should be skipped");
}
#[test]
fn foreign_key_postgres() {
let d = postgres();
let rels = [relationship(
"fk_child_parent",
"Child",
"Parent",
&[("parent_id", "id")],
0,
)];
let refs: Vec<&Relationship> = rels.iter().collect();
let result = generate_foreign_keys(&*d, &refs);
assert!(
result.contains("ALTER TABLE \"Child\" ADD CONSTRAINT \"fk_child_parent\""),
"got:\n{result}"
);
assert!(
result.contains("FOREIGN KEY (\"parent_id\") REFERENCES \"Parent\" (\"id\")"),
"got:\n{result}"
);
}
#[test]
fn foreign_key_cascade() {
use crate::relationship::relationship_flags;
let d = postgres();
let rels = [relationship(
"fk1",
"Child",
"Parent",
&[("pid", "id")],
relationship_flags::CASCADE_UPDATE | relationship_flags::CASCADE_DELETE,
)];
let refs: Vec<&Relationship> = rels.iter().collect();
let result = generate_foreign_keys(&*d, &refs);
assert!(result.contains("ON UPDATE CASCADE"), "got:\n{result}");
assert!(result.contains("ON DELETE CASCADE"), "got:\n{result}");
}
#[test]
fn foreign_key_no_cascade() {
let d = postgres();
let rels = [relationship("fk1", "Child", "Parent", &[("pid", "id")], 0)];
let refs: Vec<&Relationship> = rels.iter().collect();
let result = generate_foreign_keys(&*d, &refs);
assert!(!result.contains("ON UPDATE"), "got:\n{result}");
assert!(!result.contains("ON DELETE"), "got:\n{result}");
}
#[test]
fn create_table_sqlite_inline_fk() {
let d = sqlite();
let tdef = table(
"Child",
vec![
col_with_num(
"id",
ColumnType::Long,
0,
column_flags::FIXED | column_flags::AUTO_LONG,
0,
0,
1,
),
col_with_num(
"parent_id",
ColumnType::Long,
0,
column_flags::NULLABLE,
0,
0,
2,
),
],
vec![index(
"PrimaryKey",
&[1],
index_flags::UNIQUE | index_flags::REQUIRED,
index_type::NORMAL,
0,
)],
);
let rel = relationship("fk1", "Child", "Parent", &[("parent_id", "id")], 0);
let result = generate_create_table(&*d, &tdef, &[&rel]);
assert!(
result.contains("FOREIGN KEY (\"parent_id\") REFERENCES \"Parent\" (\"id\")"),
"got:\n{result}"
);
}
#[test]
fn generate_ddl_full_postgres() {
let d = postgres();
let tables = vec![
table(
"Parent",
vec![col_with_num("id", ColumnType::Long, 0, 0, 0, 0, 1)],
vec![index(
"PrimaryKey",
&[1],
index_flags::UNIQUE | index_flags::REQUIRED,
index_type::NORMAL,
0,
)],
),
table(
"Child",
vec![
col_with_num("id", ColumnType::Long, 0, 0, 0, 0, 1),
col_with_num("pid", ColumnType::Long, 0, column_flags::NULLABLE, 0, 0, 2),
],
vec![
index(
"PrimaryKey",
&[1],
index_flags::UNIQUE | index_flags::REQUIRED,
index_type::NORMAL,
0,
),
index("idx_pid", &[2], 0, index_type::NORMAL, 1),
],
),
];
let rels = [relationship("fk1", "Child", "Parent", &[("pid", "id")], 0)];
let result = generate_ddl(&*d, &tables, &rels, true, true);
assert!(result.contains("CREATE TABLE \"Parent\""), "got:\n{result}");
assert!(result.contains("CREATE TABLE \"Child\""), "got:\n{result}");
assert!(
result.contains("CREATE INDEX \"idx_pid\""),
"got:\n{result}"
);
assert!(
result.contains("ALTER TABLE \"Child\" ADD CONSTRAINT \"fk1\""),
"got:\n{result}"
);
}
#[test]
fn generate_ddl_no_indexes() {
let d = postgres();
let tables = vec![table(
"T",
vec![col_with_num("B", ColumnType::Long, 0, 0, 0, 0, 2)],
vec![index("idx_B", &[2], 0, index_type::NORMAL, 1)],
)];
let result = generate_ddl(&*d, &tables, &[], false, true);
assert!(!result.contains("CREATE INDEX"), "got:\n{result}");
}
#[test]
fn generate_ddl_no_relations() {
let d = postgres();
let tables = vec![table(
"T",
vec![col_with_num("id", ColumnType::Long, 0, 0, 0, 0, 1)],
vec![],
)];
let rels = vec![relationship("fk1", "T", "Other", &[("id", "id")], 0)];
let result = generate_ddl(&*d, &tables, &rels, true, false);
assert!(!result.contains("ALTER TABLE"), "got:\n{result}");
}
#[test]
fn generate_ddl_fk_filtered_by_table_set() {
let d = postgres();
let tables = vec![table(
"Table1",
vec![
col_with_num("id", ColumnType::Long, 0, 0, 0, 0, 1),
col_with_num(
"fk_col",
ColumnType::Long,
0,
column_flags::NULLABLE,
0,
0,
2,
),
],
vec![],
)];
let rels = vec![
relationship("fk_t1", "Table1", "Parent", &[("fk_col", "id")], 0),
relationship("fk_t2", "Table2", "Parent", &[("fk_col", "id")], 0),
];
let result = generate_ddl(&*d, &tables, &rels, true, true);
assert!(
result.contains("ALTER TABLE \"Table1\""),
"should include FK for Table1, got:\n{result}"
);
assert!(
!result.contains("ALTER TABLE \"Table2\""),
"should not include FK for Table2, got:\n{result}"
);
}
#[test]
fn create_table_composite_pk() {
let d = postgres();
let tdef = table(
"T",
vec![
col_with_num("a", ColumnType::Long, 0, 0, 0, 0, 1),
col_with_num("b", ColumnType::Long, 0, 0, 0, 0, 2),
],
vec![index(
"PrimaryKey",
&[1, 2],
index_flags::UNIQUE | index_flags::REQUIRED,
index_type::NORMAL,
0,
)],
);
let result = generate_create_table(&*d, &tdef, &[]);
assert!(
result.contains("PRIMARY KEY (\"a\", \"b\")"),
"got:\n{result}"
);
}
#[test]
fn access_map_byte() {
let d = access();
let c = col("x", ColumnType::Byte, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BYTE");
}
#[test]
fn access_map_int() {
let d = access();
let c = col("x", ColumnType::Int, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "SHORT");
}
#[test]
fn access_map_long() {
let d = access();
let c = col("x", ColumnType::Long, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "LONG");
}
#[test]
fn access_map_float() {
let d = access();
let c = col("x", ColumnType::Float, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "SINGLE");
}
#[test]
fn access_map_double() {
let d = access();
let c = col("x", ColumnType::Double, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "DOUBLE");
}
#[test]
fn access_map_timestamp() {
let d = access();
let c = col("x", ColumnType::Timestamp, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "DATETIME");
}
#[test]
fn access_map_binary() {
let d = access();
let c = col("x", ColumnType::Binary, 50, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BINARY(50)");
}
#[test]
fn access_map_ole() {
let d = access();
let c = col("x", ColumnType::Ole, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "OLEOBJECT");
}
#[test]
fn access_map_guid() {
let d = access();
let c = col("x", ColumnType::Guid, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "UNIQUEIDENTIFIER");
}
#[test]
fn access_map_numeric() {
let d = access();
let c = col("x", ColumnType::Numeric, 0, 0, 10, 2);
assert_eq!(d.map_column_type(&c, false), "DECIMAL(10,2)");
}
#[test]
fn access_map_complex_type() {
let d = access();
let c = col("x", ColumnType::ComplexType, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "LONG");
}
#[test]
fn access_map_bigint() {
let d = access();
let c = col("x", ColumnType::BigInt, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "LONG");
}
#[test]
fn access_map_unknown() {
let d = access();
let c = col("x", ColumnType::Unknown(0xFF), 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BINARY");
}
#[test]
fn access_map_datetime_extended() {
let d = access();
let c = col("x", ColumnType::DateTimeExtended, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "DATETIME");
}
#[test]
fn mysql_map_int() {
let d = mysql();
let c = col("x", ColumnType::Int, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "SMALLINT");
}
#[test]
fn mysql_map_long() {
let d = mysql();
let c = col("x", ColumnType::Long, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "INT");
}
#[test]
fn mysql_map_money() {
let d = mysql();
let c = col("x", ColumnType::Money, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "DECIMAL(19,4)");
}
#[test]
fn mysql_map_float() {
let d = mysql();
let c = col("x", ColumnType::Float, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "FLOAT");
}
#[test]
fn mysql_map_double() {
let d = mysql();
let c = col("x", ColumnType::Double, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "DOUBLE");
}
#[test]
fn mysql_map_timestamp() {
let d = mysql();
let c = col("x", ColumnType::Timestamp, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "DATETIME");
}
#[test]
fn mysql_map_binary() {
let d = mysql();
let c = col("x", ColumnType::Binary, 50, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "VARBINARY(50)");
}
#[test]
fn mysql_map_memo() {
let d = mysql();
let c = col("x", ColumnType::Memo, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "LONGTEXT");
}
#[test]
fn mysql_map_numeric() {
let d = mysql();
let c = col("x", ColumnType::Numeric, 0, 0, 10, 2);
assert_eq!(d.map_column_type(&c, false), "DECIMAL(10,2)");
}
#[test]
fn mysql_map_complex_type() {
let d = mysql();
let c = col("x", ColumnType::ComplexType, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "INT");
}
#[test]
fn mysql_map_bigint() {
let d = mysql();
let c = col("x", ColumnType::BigInt, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BIGINT");
}
#[test]
fn mysql_map_unknown() {
let d = mysql();
let c = col("x", ColumnType::Unknown(0xFF), 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "LONGBLOB");
}
#[test]
fn mysql_map_datetime_extended() {
let d = mysql();
let c = col("x", ColumnType::DateTimeExtended, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "DATETIME(6)");
}
#[test]
fn postgres_map_byte() {
let d = postgres();
let c = col("x", ColumnType::Byte, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "SMALLINT");
}
#[test]
fn postgres_map_int() {
let d = postgres();
let c = col("x", ColumnType::Int, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "SMALLINT");
}
#[test]
fn postgres_map_long() {
let d = postgres();
let c = col("x", ColumnType::Long, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "INTEGER");
}
#[test]
fn postgres_map_float() {
let d = postgres();
let c = col("x", ColumnType::Float, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "REAL");
}
#[test]
fn postgres_map_double() {
let d = postgres();
let c = col("x", ColumnType::Double, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "DOUBLE PRECISION");
}
#[test]
fn postgres_map_binary() {
let d = postgres();
let c = col("x", ColumnType::Binary, 50, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BYTEA");
}
#[test]
fn postgres_map_memo() {
let d = postgres();
let c = col("x", ColumnType::Memo, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "TEXT");
}
#[test]
fn postgres_map_numeric() {
let d = postgres();
let c = col("x", ColumnType::Numeric, 0, 0, 10, 2);
assert_eq!(d.map_column_type(&c, false), "NUMERIC(10,2)");
}
#[test]
fn postgres_map_complex_type() {
let d = postgres();
let c = col("x", ColumnType::ComplexType, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "INTEGER");
}
#[test]
fn postgres_map_bigint() {
let d = postgres();
let c = col("x", ColumnType::BigInt, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BIGINT");
}
#[test]
fn postgres_map_unknown() {
let d = postgres();
let c = col("x", ColumnType::Unknown(0xFF), 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BYTEA");
}
#[test]
fn postgres_map_datetime_extended() {
let d = postgres();
let c = col("x", ColumnType::DateTimeExtended, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "TIMESTAMP WITHOUT TIME ZONE");
}
#[test]
fn sqlite_map_boolean() {
let d = sqlite();
let c = col("x", ColumnType::Boolean, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "INTEGER");
}
#[test]
fn sqlite_map_byte() {
let d = sqlite();
let c = col("x", ColumnType::Byte, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "INTEGER");
}
#[test]
fn sqlite_map_int() {
let d = sqlite();
let c = col("x", ColumnType::Int, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "INTEGER");
}
#[test]
fn sqlite_map_float() {
let d = sqlite();
let c = col("x", ColumnType::Float, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "REAL");
}
#[test]
fn sqlite_map_double() {
let d = sqlite();
let c = col("x", ColumnType::Double, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "REAL");
}
#[test]
fn sqlite_map_memo() {
let d = sqlite();
let c = col("x", ColumnType::Memo, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "TEXT");
}
#[test]
fn sqlite_map_ole() {
let d = sqlite();
let c = col("x", ColumnType::Ole, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BLOB");
}
#[test]
fn sqlite_map_numeric() {
let d = sqlite();
let c = col("x", ColumnType::Numeric, 0, 0, 10, 2);
assert_eq!(d.map_column_type(&c, false), "NUMERIC");
}
#[test]
fn sqlite_map_complex_type() {
let d = sqlite();
let c = col("x", ColumnType::ComplexType, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "INTEGER");
}
#[test]
fn sqlite_map_bigint() {
let d = sqlite();
let c = col("x", ColumnType::BigInt, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "INTEGER");
}
#[test]
fn sqlite_map_unknown() {
let d = sqlite();
let c = col("x", ColumnType::Unknown(0xFF), 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "BLOB");
}
#[test]
fn sqlite_map_datetime_extended() {
let d = sqlite();
let c = col("x", ColumnType::DateTimeExtended, 0, 0, 0, 0);
assert_eq!(d.map_column_type(&c, false), "TEXT");
}
#[test]
fn japanese_identifiers_sqlite() {
let d = sqlite();
let tdef = table(
"jp_テーブル2",
vec![
col_with_num(
"ID",
ColumnType::Long,
0,
column_flags::FIXED | column_flags::AUTO_LONG,
0,
0,
1,
),
col_with_num(
"商品名",
ColumnType::Text,
255,
column_flags::NULLABLE,
0,
0,
2,
),
col_with_num("単価", ColumnType::Long, 0, column_flags::NULLABLE, 0, 0, 3),
col_with_num("個数", ColumnType::Long, 0, column_flags::NULLABLE, 0, 0, 4),
],
vec![index(
"PrimaryKey",
&[1],
index_flags::UNIQUE | index_flags::REQUIRED,
index_type::NORMAL,
0,
)],
);
let result = generate_create_table(&*d, &tdef, &[]);
assert!(
result.contains("\"jp_テーブル2\""),
"should quote Japanese table name, got:\n{result}"
);
assert!(
result.contains("\"商品名\""),
"should quote Japanese column name 商品名, got:\n{result}"
);
assert!(
result.contains("\"単価\""),
"should quote Japanese column name 単価, got:\n{result}"
);
assert!(
result.contains("\"個数\""),
"should quote Japanese column name 個数, got:\n{result}"
);
}
#[test]
fn foreign_key_multi_column() {
let d = postgres();
let rels = [relationship(
"fk_composite",
"Child",
"Parent",
&[("a", "x"), ("b", "y")],
0,
)];
let refs: Vec<&Relationship> = rels.iter().collect();
let result = generate_foreign_keys(&*d, &refs);
assert!(
result.contains("FOREIGN KEY (\"a\", \"b\") REFERENCES \"Parent\" (\"x\", \"y\")"),
"got:\n{result}"
);
}
}