use crate::sqlite::ddl::{
CheckConstraint, Column, ForeignKey, Index, PrimaryKey, Table, TableSql, UniqueConstraint, View,
};
use serde::{Deserialize, Serialize};
pub const BREAKPOINT: &str = "--> statement-breakpoint";
fn quote_ident(ident: &str) -> String {
format!("`{}`", ident.replace('`', "``"))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TableFull {
pub name: String,
pub columns: Vec<Column>,
pub pk: Option<PrimaryKey>,
pub fks: Vec<ForeignKey>,
pub uniques: Vec<UniqueConstraint>,
pub checks: Vec<CheckConstraint>,
#[serde(default)]
pub strict: bool,
#[serde(default)]
pub without_rowid: bool,
}
impl TableFull {
#[must_use]
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
columns: Vec::new(),
pk: None,
fks: Vec::new(),
uniques: Vec::new(),
checks: Vec::new(),
strict: false,
without_rowid: false,
}
}
}
fn table_full_to_table(table: &TableFull) -> Table {
Table {
name: table.name.clone().into(),
strict: table.strict,
without_rowid: table.without_rowid,
}
}
fn create_table_sql(table: &TableFull) -> String {
let ddl_table = table_full_to_table(table);
TableSql::new(&ddl_table)
.columns(&table.columns)
.primary_key(table.pk.as_ref())
.foreign_keys(&table.fks)
.unique_constraints(&table.uniques)
.check_constraints(&table.checks)
.create_table_sql()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum JsonStatement {
CreateTable(CreateTableStatement),
DropTable(DropTableStatement),
RenameTable(RenameTableStatement),
AddColumn(AddColumnStatement),
DropColumn(DropColumnStatement),
RenameColumn(RenameColumnStatement),
RecreateColumn(RecreateColumnStatement),
RecreateTable(RecreateTableStatement),
CreateIndex(CreateIndexStatement),
DropIndex(DropIndexStatement),
CreateView(CreateViewStatement),
DropView(DropViewStatement),
RenameView(RenameViewStatement),
}
impl JsonStatement {
#[must_use]
pub const fn type_name(&self) -> &'static str {
match self {
Self::CreateTable(_) => "create_table",
Self::DropTable(_) => "drop_table",
Self::RenameTable(_) => "rename_table",
Self::AddColumn(_) => "add_column",
Self::DropColumn(_) => "drop_column",
Self::RenameColumn(_) => "rename_column",
Self::RecreateColumn(_) => "recreate_column",
Self::RecreateTable(_) => "recreate_table",
Self::CreateIndex(_) => "create_index",
Self::DropIndex(_) => "drop_index",
Self::CreateView(_) => "create_view",
Self::DropView(_) => "drop_view",
Self::RenameView(_) => "rename_view",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateTableStatement {
pub table: TableFull,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DropTableStatement {
pub table_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RenameTableStatement {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AddColumnStatement {
pub column: Column,
pub fk: Option<ForeignKey>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DropColumnStatement {
pub column: Column,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RenameColumnStatement {
pub table: String,
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecreateColumnStatement {
pub column: Column,
pub fk: Option<ForeignKey>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecreateTableStatement {
pub from: TableFull,
pub to: TableFull,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateIndexStatement {
pub index: Index,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DropIndexStatement {
pub index: Index,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateViewStatement {
pub view: View,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DropViewStatement {
pub view: View,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RenameViewStatement {
pub from: View,
pub to: View,
}
#[must_use]
pub fn convert_statement(statement: &JsonStatement) -> Vec<String> {
match statement {
JsonStatement::CreateTable(st) => vec![convert_create_table(st)],
JsonStatement::DropTable(st) => vec![convert_drop_table(st)],
JsonStatement::RenameTable(st) => vec![convert_rename_table(st)],
JsonStatement::AddColumn(st) => vec![convert_add_column(st)],
JsonStatement::DropColumn(st) => vec![convert_drop_column(st)],
JsonStatement::RenameColumn(st) => vec![convert_rename_column(st)],
JsonStatement::RecreateColumn(st) => convert_recreate_column(st),
JsonStatement::RecreateTable(st) => convert_recreate_table(st),
JsonStatement::CreateIndex(st) => vec![convert_create_index(st)],
JsonStatement::DropIndex(st) => vec![convert_drop_index(st)],
JsonStatement::CreateView(st) => vec![convert_create_view(st)],
JsonStatement::DropView(st) => vec![convert_drop_view(st)],
JsonStatement::RenameView(st) => vec![convert_rename_view(st)],
}
}
pub fn statements_to_sql(statements: &[JsonStatement], breakpoints: bool) -> String {
let sql_statements: Vec<String> = statements.iter().flat_map(convert_statement).collect();
if breakpoints {
sql_statements.join(&format!("\n{BREAKPOINT}\n"))
} else {
sql_statements.join("\n")
}
}
#[derive(Debug, Clone)]
pub struct GroupedStatement {
pub json_statement: JsonStatement,
pub sql_statements: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ConversionResult {
pub sql_statements: Vec<String>,
pub grouped_statements: Vec<GroupedStatement>,
}
#[must_use]
pub fn from_json(statements: Vec<JsonStatement>) -> ConversionResult {
let grouped: Vec<GroupedStatement> = statements
.into_iter()
.map(|statement| {
let sql_statements = convert_statement(&statement);
GroupedStatement {
json_statement: statement,
sql_statements,
}
})
.collect();
let sql_statements: Vec<String> = grouped
.iter()
.flat_map(|g| g.sql_statements.clone())
.collect();
ConversionResult {
sql_statements,
grouped_statements: grouped,
}
}
fn convert_create_table(st: &CreateTableStatement) -> String {
create_table_sql(&st.table)
}
fn convert_drop_table(st: &DropTableStatement) -> String {
format!("DROP TABLE {};", quote_ident(&st.table_name))
}
fn convert_rename_table(st: &RenameTableStatement) -> String {
format!(
"ALTER TABLE {} RENAME TO {};",
quote_ident(&st.from),
quote_ident(&st.to)
)
}
fn convert_add_column(st: &AddColumnStatement) -> String {
let column = &st.column;
let column_def = column.to_column_sql(false, false);
let reference = st
.fk
.as_ref()
.map(|fk| {
let to_cols = fk
.columns_to
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(",");
if fk.name_explicit {
format!(
" CONSTRAINT {} REFERENCES {}({})",
quote_ident(&fk.name),
quote_ident(&fk.table_to),
to_cols
)
} else {
format!(" REFERENCES {}({})", quote_ident(&fk.table_to), to_cols)
}
})
.unwrap_or_default();
format!(
"ALTER TABLE {} ADD {}{};",
quote_ident(&column.table),
column_def,
reference
)
}
fn convert_drop_column(st: &DropColumnStatement) -> String {
format!(
"ALTER TABLE {} DROP COLUMN {};",
quote_ident(&st.column.table),
quote_ident(&st.column.name)
)
}
fn convert_rename_column(st: &RenameColumnStatement) -> String {
format!(
"ALTER TABLE {} RENAME COLUMN {} TO {};",
quote_ident(&st.table),
quote_ident(&st.from),
quote_ident(&st.to)
)
}
fn convert_recreate_column(st: &RecreateColumnStatement) -> Vec<String> {
let drop = format!(
"ALTER TABLE {} DROP COLUMN {};",
quote_ident(&st.column.table),
quote_ident(&st.column.name)
);
let add = convert_add_column(&AddColumnStatement {
column: st.column.clone(),
fk: st.fk.clone(),
});
vec![drop, add]
}
fn convert_recreate_table(st: &RecreateTableStatement) -> Vec<String> {
let name = &st.to.name;
let new_table_name = format!("__new_{name}");
let column_names: Vec<String> = st
.from
.columns
.iter()
.filter(|col| {
col.generated.is_none()
&& st
.to
.columns
.iter()
.any(|c| c.name == col.name && c.generated.is_none())
})
.map(|col| quote_ident(&col.name))
.collect();
let cols_str = column_names.join(", ");
let mut statements = Vec::new();
statements.push("PRAGMA foreign_keys=OFF;".to_string());
let mut tmp_table = st.to.clone();
tmp_table.name.clone_from(&new_table_name);
for check in &mut tmp_table.checks {
check.table = new_table_name.clone().into();
}
statements.push(convert_create_table(&CreateTableStatement {
table: tmp_table,
}));
statements.push(format!(
"INSERT INTO {}({cols_str}) SELECT {cols_str} FROM {};",
quote_ident(&new_table_name),
quote_ident(name)
));
statements.push(format!("DROP TABLE {};", quote_ident(name)));
statements.push(format!(
"ALTER TABLE {} RENAME TO {};",
quote_ident(&new_table_name),
quote_ident(name)
));
statements.push("PRAGMA foreign_keys=ON;".to_string());
statements
}
fn convert_create_index(st: &CreateIndexStatement) -> String {
st.index.create_index_sql()
}
fn convert_drop_index(st: &DropIndexStatement) -> String {
format!("DROP INDEX IF EXISTS {};", quote_ident(&st.index.name))
}
fn convert_create_view(st: &CreateViewStatement) -> String {
st.view.create_view_sql()
}
fn convert_drop_view(st: &DropViewStatement) -> String {
format!("DROP VIEW {};", quote_ident(&st.view.name))
}
fn convert_rename_view(st: &RenameViewStatement) -> String {
format!(
"DROP VIEW IF EXISTS {};\n{}",
quote_ident(&st.from.name),
st.to.create_view_sql()
)
}
#[must_use]
pub fn prepare_add_columns(columns: &[Column], fks: &[ForeignKey]) -> Vec<AddColumnStatement> {
columns
.iter()
.map(|col| {
let fk = fks
.iter()
.find(|fk| {
fk.columns.len() == 1 && fk.columns[0] == col.name && fk.table == col.table
})
.cloned();
AddColumnStatement {
column: col.clone(),
fk,
}
})
.collect()
}
use crate::sqlite::SchemaDiff;
use crate::sqlite::collection::{DiffType, EntityDiff};
use crate::traits::EntityKind;
use std::collections::{HashMap, HashSet};
pub struct TopologicalSortResult<'a> {
pub tables: Vec<&'a EntityDiff>,
pub has_circular_deps: bool,
}
fn topological_sort_tables_for_create<'a>(
tables: &[&'a EntityDiff],
diff: &SchemaDiff,
) -> TopologicalSortResult<'a> {
if tables.len() <= 1 {
return TopologicalSortResult {
tables: tables.to_vec(),
has_circular_deps: false,
};
}
let mut table_map: HashMap<String, &EntityDiff> = HashMap::new();
for t in tables {
if let Some(name) = t.name.split(':').next_back() {
table_map.insert(name.to_string(), *t);
}
}
let mut dependencies: HashMap<String, HashSet<String>> = HashMap::new();
for table_name in table_map.keys() {
dependencies.insert(table_name.clone(), HashSet::new());
}
for fk_diff in diff.by_kind(EntityKind::ForeignKey) {
if fk_diff.diff_type == DiffType::Create
&& let Some(crate::sqlite::ddl::SqliteEntity::ForeignKey(fk)) = fk_diff.right.as_ref()
{
let from_table = fk.table.to_string();
let to_table = fk.table_to.to_string();
if table_map.contains_key(&from_table)
&& table_map.contains_key(&to_table)
&& let Some(deps) = dependencies.get_mut(&from_table)
{
deps.insert(to_table);
}
}
}
let mut result = Vec::new();
let mut remaining: HashSet<String> = table_map.keys().cloned().collect();
let mut satisfied: HashSet<String> = HashSet::new();
let mut has_circular_deps = false;
while !remaining.is_empty() {
let ready: Vec<String> = remaining
.iter()
.filter(|t| {
dependencies
.get(*t)
.is_none_or(|deps| deps.iter().all(|d| satisfied.contains(d)))
})
.cloned()
.collect();
if ready.is_empty() {
has_circular_deps = true;
for t in &remaining {
if let Some(entity) = table_map.get(t) {
result.push(*entity);
}
}
break;
}
for t in ready {
remaining.remove(&t);
satisfied.insert(t.clone());
if let Some(entity) = table_map.get(&t) {
result.push(*entity);
}
}
}
TopologicalSortResult {
tables: result,
has_circular_deps,
}
}
fn topological_sort_tables_for_drop<'a>(
tables: &[&'a EntityDiff],
diff: &SchemaDiff,
) -> TopologicalSortResult<'a> {
let create_result = topological_sort_tables_for_create(tables, diff);
TopologicalSortResult {
tables: create_result.tables.into_iter().rev().collect(),
has_circular_deps: create_result.has_circular_deps,
}
}
fn append_drop_table_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
let dropped_tables = diff.dropped_tables();
let drop_result = topological_sort_tables_for_drop(&dropped_tables, diff);
if drop_result.has_circular_deps && !drop_result.tables.is_empty() {
statements.push("PRAGMA foreign_keys=OFF;".to_string());
}
for entity_diff in &drop_result.tables {
if let Some(name) = entity_diff.name.split(':').next_back() {
statements.push(convert_drop_table(&DropTableStatement {
table_name: name.to_string(),
}));
}
}
if drop_result.has_circular_deps && !drop_result.tables.is_empty() {
statements.push("PRAGMA foreign_keys=ON;".to_string());
}
}
fn build_table_full(diff: &SchemaDiff, table: &crate::sqlite::ddl::Table) -> TableFull {
let columns_for_table: Vec<Column> = diff
.by_kind(EntityKind::Column)
.into_iter()
.filter(|d| d.diff_type == DiffType::Create)
.filter_map(|d| d.right.as_ref())
.filter_map(|e| match e {
crate::sqlite::ddl::SqliteEntity::Column(c) if c.table == table.name => Some(c.clone()),
_ => None,
})
.collect();
let pk = diff
.by_kind(EntityKind::PrimaryKey)
.into_iter()
.filter(|d| d.diff_type == DiffType::Create)
.filter_map(|d| d.right.as_ref())
.find_map(|e| match e {
crate::sqlite::ddl::SqliteEntity::PrimaryKey(p) if p.table == table.name => {
Some(p.clone())
}
_ => None,
});
let fks: Vec<ForeignKey> = diff
.by_kind(EntityKind::ForeignKey)
.into_iter()
.filter(|d| d.diff_type == DiffType::Create)
.filter_map(|d| d.right.as_ref())
.filter_map(|e| match e {
crate::sqlite::ddl::SqliteEntity::ForeignKey(fk) if fk.table == table.name => {
Some(fk.clone())
}
_ => None,
})
.collect();
let uniques: Vec<UniqueConstraint> = diff
.by_kind(EntityKind::UniqueConstraint)
.into_iter()
.filter(|d| d.diff_type == DiffType::Create)
.filter_map(|d| d.right.as_ref())
.filter_map(|e| match e {
crate::sqlite::ddl::SqliteEntity::UniqueConstraint(u) if u.table == table.name => {
Some(u.clone())
}
_ => None,
})
.collect();
let checks: Vec<CheckConstraint> = diff
.by_kind(EntityKind::CheckConstraint)
.into_iter()
.filter(|d| d.diff_type == DiffType::Create)
.filter_map(|d| d.right.as_ref())
.filter_map(|e| match e {
crate::sqlite::ddl::SqliteEntity::CheckConstraint(c) if c.table == table.name => {
Some(c.clone())
}
_ => None,
})
.collect();
TableFull {
name: table.name.to_string(),
columns: columns_for_table,
pk,
fks,
uniques,
checks,
strict: table.strict,
without_rowid: table.without_rowid,
}
}
fn append_create_table_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
let created_tables = diff.created_tables();
let create_result = topological_sort_tables_for_create(&created_tables, diff);
if create_result.has_circular_deps && !create_result.tables.is_empty() {
statements.push("PRAGMA foreign_keys=OFF;".to_string());
}
for entity_diff in &create_result.tables {
if let Some(crate::sqlite::ddl::SqliteEntity::Table(table)) = entity_diff.right.as_ref() {
let table_full = build_table_full(diff, table);
statements.push(convert_create_table(&CreateTableStatement {
table: table_full,
}));
}
}
if create_result.has_circular_deps && !create_result.tables.is_empty() {
statements.push("PRAGMA foreign_keys=ON;".to_string());
}
}
fn append_add_column_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
for entity_diff in diff.by_kind(EntityKind::Column) {
if entity_diff.diff_type == DiffType::Create
&& let Some(crate::sqlite::ddl::SqliteEntity::Column(col)) = entity_diff.right.as_ref()
{
let table_was_created = diff.created_tables().iter().any(|t| t.name == col.table);
if !table_was_created {
statements.push(convert_add_column(&AddColumnStatement {
column: col.clone(),
fk: None,
}));
}
}
}
}
fn append_index_stmts(statements: &mut Vec<String>, diff: &SchemaDiff) {
for entity_diff in diff.by_kind(EntityKind::Index) {
match entity_diff.diff_type {
DiffType::Drop => {
if let Some(crate::sqlite::ddl::SqliteEntity::Index(idx)) =
entity_diff.left.as_ref()
{
statements.push(convert_drop_index(&DropIndexStatement {
index: idx.clone(),
}));
}
}
DiffType::Create => {
if let Some(crate::sqlite::ddl::SqliteEntity::Index(idx)) =
entity_diff.right.as_ref()
{
statements.push(convert_create_index(&CreateIndexStatement {
index: idx.clone(),
}));
}
}
DiffType::Alter => {
if let (
Some(crate::sqlite::ddl::SqliteEntity::Index(old)),
Some(crate::sqlite::ddl::SqliteEntity::Index(new)),
) = (entity_diff.left.as_ref(), entity_diff.right.as_ref())
{
statements.push(convert_drop_index(&DropIndexStatement {
index: old.clone(),
}));
statements.push(convert_create_index(&CreateIndexStatement {
index: new.clone(),
}));
}
}
}
}
}
pub struct SqliteGenerator {
pub breakpoints: bool,
}
impl Default for SqliteGenerator {
fn default() -> Self {
Self::new()
}
}
impl SqliteGenerator {
#[must_use]
pub const fn new() -> Self {
Self { breakpoints: true }
}
#[must_use]
pub const fn with_breakpoints(mut self, breakpoints: bool) -> Self {
self.breakpoints = breakpoints;
self
}
#[must_use]
pub fn generate_migration(&self, diff: &SchemaDiff) -> Vec<String> {
let mut statements = Vec::new();
append_drop_table_stmts(&mut statements, diff);
append_create_table_stmts(&mut statements, diff);
append_add_column_stmts(&mut statements, diff);
append_index_stmts(&mut statements, diff);
statements
}
#[must_use]
pub fn statements_to_sql(&self, statements: &[String]) -> String {
if self.breakpoints {
statements.join(&format!("\n{BREAKPOINT}\n"))
} else {
statements.join("\n")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sqlite::ddl::{Column, IndexColumn};
#[test]
fn test_create_table_simple() {
let table = TableFull {
name: "users".to_string(),
columns: vec![
Column::new("users", "id", "integer").not_null(),
Column::new("users", "name", "text").not_null(),
],
pk: None,
fks: Vec::new(),
uniques: Vec::new(),
checks: Vec::new(),
strict: false,
without_rowid: false,
};
let sql = convert_create_table(&CreateTableStatement { table });
assert_eq!(
sql,
"CREATE TABLE `users` (\n\t`id` INTEGER NOT NULL,\n\t`name` TEXT NOT NULL\n);"
);
}
#[test]
fn test_create_table_with_pk() {
let table = TableFull {
name: "users".to_string(),
columns: vec![Column::new("users", "id", "integer")],
pk: Some(PrimaryKey::from_strings(
"users".to_string(),
"users_pk".to_string(),
vec!["id".to_string()],
)),
fks: Vec::new(),
uniques: Vec::new(),
checks: Vec::new(),
strict: false,
without_rowid: false,
};
let sql = convert_create_table(&CreateTableStatement { table });
assert_eq!(
sql,
"CREATE TABLE `users` (\n\t`id` INTEGER PRIMARY KEY\n);"
);
}
#[test]
fn test_drop_table() {
let sql = convert_drop_table(&DropTableStatement {
table_name: "users".to_string(),
});
assert_eq!(sql, "DROP TABLE `users`;");
}
#[test]
fn test_add_column() {
let sql = convert_add_column(&AddColumnStatement {
column: Column::new("users", "email", "text").not_null(),
fk: None,
});
assert_eq!(sql, "ALTER TABLE `users` ADD `email` TEXT NOT NULL;");
}
#[test]
fn test_create_index() {
let index = Index::new(
"users",
"idx_users_email",
vec![IndexColumn {
value: "email".into(),
is_expression: false,
}],
)
.unique();
let sql = convert_create_index(&CreateIndexStatement { index });
assert_eq!(
sql,
"CREATE UNIQUE INDEX `idx_users_email` ON `users`(`email`);"
);
}
#[test]
fn test_create_table_strict() {
let table = TableFull {
name: "data".to_string(),
columns: vec![
Column::new("data", "id", "integer").not_null(),
Column::new("data", "value", "text").not_null(),
],
pk: None,
fks: Vec::new(),
uniques: Vec::new(),
checks: Vec::new(),
strict: true,
without_rowid: false,
};
let sql = convert_create_table(&CreateTableStatement { table });
assert_eq!(
sql,
"CREATE TABLE `data` (\n\t`id` INTEGER NOT NULL,\n\t`value` TEXT NOT NULL\n) STRICT;"
);
}
#[test]
fn test_create_table_without_rowid() {
let table = TableFull {
name: "kv".to_string(),
columns: vec![
Column::new("kv", "key", "text").not_null(),
Column::new("kv", "value", "blob"),
],
pk: Some(PrimaryKey::from_strings(
"kv".to_string(),
"kv_pk".to_string(),
vec!["key".to_string()],
)),
fks: Vec::new(),
uniques: Vec::new(),
checks: Vec::new(),
strict: false,
without_rowid: true,
};
let sql = convert_create_table(&CreateTableStatement { table });
assert_eq!(
sql,
"CREATE TABLE `kv` (\n\t`key` TEXT PRIMARY KEY NOT NULL,\n\t`value` BLOB\n) WITHOUT ROWID;"
);
}
#[test]
fn test_create_table_strict_without_rowid() {
let table = TableFull {
name: "cache".to_string(),
columns: vec![
Column::new("cache", "key", "text").not_null(),
Column::new("cache", "data", "blob"),
],
pk: Some(PrimaryKey::from_strings(
"cache".to_string(),
"cache_pk".to_string(),
vec!["key".to_string()],
)),
fks: Vec::new(),
uniques: Vec::new(),
checks: Vec::new(),
strict: true,
without_rowid: true,
};
let sql = convert_create_table(&CreateTableStatement { table });
assert_eq!(
sql,
"CREATE TABLE `cache` (\n\t`key` TEXT PRIMARY KEY NOT NULL,\n\t`data` BLOB\n) WITHOUT ROWID, STRICT;"
);
}
}