use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::Database;
#[test]
fn test_save_sql_dump() {
let mut db = Database::new();
let schema = TableSchema::new(
"test".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"name".to_string(),
DataType::Varchar { max_length: Some(50) },
false,
),
],
);
db.create_table(schema).unwrap();
let table = db.get_table_mut("test").unwrap();
table
.insert(crate::Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("Alice")),
]))
.unwrap();
table
.insert(crate::Row::new(vec![
SqlValue::Integer(2),
SqlValue::Varchar(arcstr::ArcStr::from("Bob")),
]))
.unwrap();
let path = "/tmp/test_db.sql";
db.save_sql_dump(path).unwrap();
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("CREATE TABLE"));
assert!(content.contains("INSERT INTO"));
assert!(content.contains("Alice"));
assert!(content.contains("Bob"));
std::fs::remove_file(path).ok();
}
#[test]
fn test_sql_dump_emits_verbatim_create_source() {
let mut db = Database::new();
let mut schema = TableSchema::new(
"test1".to_string(),
vec![
ColumnSchema::new("one".to_string(), DataType::Varchar { max_length: Some(10) }, true),
ColumnSchema::new("two".to_string(), DataType::Varchar { max_length: None }, true),
],
);
let verbatim = "CREATE TABLE test1 (\n one varchar(10),\n two text\n )";
schema.set_sql_source(verbatim);
db.create_table(schema).unwrap();
let path = "/tmp/test_db_verbatim_5619.sql";
db.save_sql_dump(path).unwrap();
let content = std::fs::read_to_string(path).unwrap();
assert!(
content.contains(verbatim),
"dump should contain the verbatim CREATE TABLE text, got:\n{}",
content
);
std::fs::remove_file(path).ok();
}
#[test]
fn test_sql_value_to_literal() {
use crate::persistence::save::sql_value_to_literal;
assert_eq!(sql_value_to_literal(&SqlValue::Null), "NULL");
assert_eq!(sql_value_to_literal(&SqlValue::Integer(42)), "42");
assert_eq!(sql_value_to_literal(&SqlValue::Varchar(arcstr::ArcStr::from("test"))), "'test'");
assert_eq!(
sql_value_to_literal(&SqlValue::Varchar(arcstr::ArcStr::from("test's"))),
"'test''s'"
);
assert_eq!(sql_value_to_literal(&SqlValue::Boolean(true)), "TRUE");
assert_eq!(sql_value_to_literal(&SqlValue::Boolean(false)), "FALSE");
}
#[test]
fn test_sql_dump_with_nulls() {
let mut db = Database::new();
let schema = TableSchema::new(
"test_nulls".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"nullable_col".to_string(),
DataType::Varchar { max_length: Some(50) },
true,
),
],
);
db.create_table(schema).unwrap();
let table = db.get_table_mut("test_nulls").unwrap();
table.insert(crate::Row::new(vec![SqlValue::Integer(1), SqlValue::Null])).unwrap();
table
.insert(crate::Row::new(vec![
SqlValue::Integer(2),
SqlValue::Varchar(arcstr::ArcStr::from("text")),
]))
.unwrap();
table.insert(crate::Row::new(vec![SqlValue::Integer(3), SqlValue::Null])).unwrap();
let path = "/tmp/test_nulls.sql";
db.save_sql_dump(path).unwrap();
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("NULL"), "SQL dump should contain NULL literal");
assert!(content.contains("INSERT INTO test_nulls VALUES (1, NULL)"));
assert!(content.contains("INSERT INTO test_nulls VALUES (2, 'text')"));
assert!(content.contains("INSERT INTO test_nulls VALUES (3, NULL)"));
std::fs::remove_file(path).ok();
}
#[test]
fn test_sql_dump_with_quotes() {
let mut db = Database::new();
let schema = TableSchema::new(
"test_quotes".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"text".to_string(),
DataType::Varchar { max_length: Some(100) },
false,
),
],
);
db.create_table(schema).unwrap();
let table = db.get_table_mut("test_quotes").unwrap();
table
.insert(crate::Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("it's")),
]))
.unwrap();
table
.insert(crate::Row::new(vec![
SqlValue::Integer(2),
SqlValue::Varchar(arcstr::ArcStr::from("say \"hello\"")),
]))
.unwrap();
table
.insert(crate::Row::new(vec![
SqlValue::Integer(3),
SqlValue::Varchar(arcstr::ArcStr::from("it's a \"test\"")),
]))
.unwrap();
table
.insert(crate::Row::new(vec![
SqlValue::Integer(4),
SqlValue::Varchar(arcstr::ArcStr::from("")),
]))
.unwrap();
let path = "/tmp/test_quotes.sql";
db.save_sql_dump(path).unwrap();
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("'it''s'"), "Single quotes should be escaped as ''");
assert!(
content.contains("'say \"hello\"'"),
"Double quotes should be preserved in single-quoted strings"
);
assert!(content.contains("'it''s a \"test\"'"), "Mixed quotes should be handled correctly");
assert!(content.contains("''"), "Empty string should be represented");
std::fs::remove_file(path).ok();
}
#[test]
fn test_sql_dump_empty_database() {
let db = Database::new();
let path = "/tmp/test_empty_db.sql";
db.save_sql_dump(path).unwrap();
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("-- VibeSQL Database Dump"));
assert!(content.contains("-- Schemas"));
assert!(content.contains("-- Roles"));
assert!(content.contains("-- Tables and Data"));
assert!(content.contains("-- Indexes"));
assert!(content.contains("-- End of dump"));
assert!(!content.contains("CREATE TABLE"));
std::fs::remove_file(path).ok();
}
#[test]
fn test_sql_dump_empty_table() {
let mut db = Database::new();
let schema = TableSchema::new(
"empty_table".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"name".to_string(),
DataType::Varchar { max_length: Some(50) },
false,
),
],
);
db.create_table(schema).unwrap();
let path = "/tmp/test_empty_table.sql";
db.save_sql_dump(path).unwrap();
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("CREATE TABLE empty_table"));
assert!(
!content.contains("INSERT INTO empty_table"),
"Empty table should have no INSERT statements"
);
std::fs::remove_file(path).ok();
}
#[test]
fn test_sql_dump_with_indexes() {
let mut db = Database::new();
let schema = TableSchema::new(
"test_indexes".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"name".to_string(),
DataType::Varchar { max_length: Some(50) },
false,
),
ColumnSchema::new(
"email".to_string(),
DataType::Varchar { max_length: Some(100) },
false,
),
],
);
db.create_table(schema).unwrap();
let idx1 = vibesql_ast::IndexColumn::Column {
column_name: "name".to_string(),
direction: vibesql_ast::OrderDirection::Asc,
prefix_length: None,
};
db.create_index("idx_name".to_string(), "test_indexes".to_string(), false, vec![idx1]).unwrap();
let idx2 = vibesql_ast::IndexColumn::Column {
column_name: "email".to_string(),
direction: vibesql_ast::OrderDirection::Asc,
prefix_length: None,
};
db.create_index("idx_email_unique".to_string(), "test_indexes".to_string(), true, vec![idx2])
.unwrap();
let path = "/tmp/test_indexes.sql";
db.save_sql_dump(path).unwrap();
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("CREATE INDEX idx_name"), "Should contain CREATE INDEX idx_name");
assert!(
content.contains("CREATE UNIQUE INDEX idx_email_unique"),
"Should contain CREATE UNIQUE INDEX idx_email_unique"
);
assert!(content.contains("ON test_indexes"), "Should reference test_indexes table");
assert!(
!content.contains("Asc"),
"Debug-formatted sort direction must not leak into the SQL dump"
);
std::fs::remove_file(path).ok();
}
#[test]
fn test_sql_dump_index_quoted_identifiers_roundtrip() {
use crate::persistence::load::read_sql_dump;
let mut db = Database::new();
let schema = TableSchema::new(
"t1'x1".to_string(),
vec![
ColumnSchema::new("b".to_string(), DataType::Integer, false),
ColumnSchema::new("c".to_string(), DataType::Integer, false),
],
);
db.create_table(schema).unwrap();
let cols = vec![
vibesql_ast::IndexColumn::Column {
column_name: "b".to_string(),
direction: vibesql_ast::OrderDirection::Desc,
prefix_length: None,
},
vibesql_ast::IndexColumn::Column {
column_name: "c".to_string(),
direction: vibesql_ast::OrderDirection::Asc,
prefix_length: None,
},
];
db.create_index("i3".to_string(), "t1'x1".to_string(), false, cols).unwrap();
let path = "/tmp/test_index_quoted_roundtrip.sql";
db.save_sql_dump(path).unwrap();
let content = read_sql_dump(path).unwrap();
assert!(
content.contains("ON \"t1'x1\""),
"Index DDL must quote the table name. Got:\n{content}"
);
assert!(!content.contains("Asc"), "Debug sort direction leaked:\n{content}");
assert!(content.contains("DESC"), "DESC direction should be emitted:\n{content}");
let create_index_stmt = content
.lines()
.find(|l| l.trim_start().starts_with("CREATE INDEX"))
.unwrap_or_else(|| panic!("No CREATE INDEX line in dump:\n{content}"));
vibesql_parser::Parser::parse_sql(create_index_stmt).unwrap_or_else(|e| {
panic!("Emitted index DDL must re-parse: {e}\nStatement: {create_index_stmt}")
});
std::fs::remove_file(path).ok();
}
#[test]
fn test_sql_dump_preserves_index_name_case() {
use crate::persistence::load::read_sql_dump;
let mut db = Database::new();
let schema = TableSchema::new(
"t".to_string(),
vec![
ColumnSchema::new("b".to_string(), DataType::Integer, false),
ColumnSchema::new("c".to_string(), DataType::Integer, false),
],
);
db.create_table(schema).unwrap();
let cols1 = vec![vibesql_ast::IndexColumn::Column {
column_name: "b".to_string(),
direction: vibesql_ast::OrderDirection::Asc,
prefix_length: None,
}];
db.create_index("MyIdx".to_string(), "t".to_string(), false, cols1).unwrap();
let cols2 = vec![
vibesql_ast::IndexColumn::Column {
column_name: "b".to_string(),
direction: vibesql_ast::OrderDirection::Desc,
prefix_length: None,
},
vibesql_ast::IndexColumn::Column {
column_name: "c".to_string(),
direction: vibesql_ast::OrderDirection::Asc,
prefix_length: None,
},
];
db.create_index("x1 (b Asc, c Asc)".to_string(), "t".to_string(), false, cols2).unwrap();
let path = "/tmp/test_index_name_case.sql";
db.save_sql_dump(path).unwrap();
let content = read_sql_dump(path).unwrap();
assert!(
content.contains("CREATE INDEX MyIdx ON"),
"Index name case must be preserved (expected `MyIdx`). Got:\n{content}"
);
assert!(!content.contains("myidx"), "Index name must not be lowercased. Got:\n{content}");
assert!(
content.contains("\"x1 (b Asc, c Asc)\""),
"Quoted index name must preserve original case. Got:\n{content}"
);
assert!(
!content.contains("x1 (b asc, c asc)"),
"Quoted index name must not be lowercased. Got:\n{content}"
);
assert!(db.index_exists("myidx"), "case-insensitive lookup must still work");
for line in content.lines().filter(|l| l.trim_start().starts_with("CREATE INDEX")) {
vibesql_parser::Parser::parse_sql(line)
.unwrap_or_else(|e| panic!("Emitted index DDL must re-parse: {e}\nStatement: {line}"));
}
db.drop_index("MYIDX").expect("case-insensitive DROP must drop the mixed-case index");
assert!(!db.index_exists("MyIdx"), "index should be gone after case-insensitive drop");
std::fs::remove_file(path).ok();
}
#[test]
fn test_sql_dump_all_data_types() {
let mut db = Database::new();
let schema = TableSchema::new(
"all_types".to_string(),
vec![
ColumnSchema::new("col_int".to_string(), DataType::Integer, true),
ColumnSchema::new("col_smallint".to_string(), DataType::Smallint, true),
ColumnSchema::new("col_bigint".to_string(), DataType::Bigint, true),
ColumnSchema::new("col_float".to_string(), DataType::Float { precision: 24 }, true),
ColumnSchema::new("col_real".to_string(), DataType::Real, true),
ColumnSchema::new("col_double".to_string(), DataType::DoublePrecision, true),
ColumnSchema::new(
"col_varchar".to_string(),
DataType::Varchar { max_length: Some(100) },
true,
),
ColumnSchema::new("col_char".to_string(), DataType::Character { length: 10 }, true),
ColumnSchema::new("col_bool".to_string(), DataType::Boolean, true),
ColumnSchema::new(
"col_numeric".to_string(),
DataType::Numeric { precision: 10, scale: 2 },
true,
),
ColumnSchema::new(
"col_decimal".to_string(),
DataType::Decimal { precision: 5, scale: 0 },
true,
),
],
);
db.create_table(schema).unwrap();
let table = db.get_table_mut("all_types").unwrap();
table
.insert(crate::Row::new(vec![
SqlValue::Integer(42),
SqlValue::Smallint(100),
SqlValue::Bigint(999999),
SqlValue::Float(3.14),
SqlValue::Real(2.718),
SqlValue::Double(1.414),
SqlValue::Varchar(arcstr::ArcStr::from("test")),
SqlValue::Character(arcstr::ArcStr::from("fixed")),
SqlValue::Boolean(true),
SqlValue::Numeric(123.45),
SqlValue::Numeric(999.0),
]))
.unwrap();
table
.insert(crate::Row::new(vec![
SqlValue::Null,
SqlValue::Null,
SqlValue::Null,
SqlValue::Float(f32::NAN),
SqlValue::Real(f64::INFINITY),
SqlValue::Double(f64::NEG_INFINITY),
SqlValue::Null,
SqlValue::Null,
SqlValue::Boolean(false),
SqlValue::Null,
SqlValue::Null,
]))
.unwrap();
let path = "/tmp/test_all_types.sql";
db.save_sql_dump(path).unwrap();
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("INTEGER"));
assert!(content.contains("SMALLINT"));
assert!(content.contains("BIGINT"));
assert!(content.contains("FLOAT(24)"));
assert!(content.contains("REAL"));
assert!(content.contains("DOUBLE PRECISION"));
assert!(content.contains("VARCHAR(100)"));
assert!(content.contains("CHAR(10)"));
assert!(content.contains("BOOLEAN"));
assert!(content.contains("NUMERIC(10, 2)"));
assert!(content.contains("DECIMAL(5, 0)"));
assert!(content.contains("'NaN'"));
assert!(content.contains("'Infinity'"));
assert!(content.contains("'-Infinity'"));
assert!(content.contains("TRUE"));
assert!(content.contains("FALSE"));
std::fs::remove_file(path).ok();
}
#[test]
fn test_sql_dump_large_dataset() {
let mut db = Database::new();
let schema = TableSchema::new(
"large_table".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"value".to_string(),
DataType::Varchar { max_length: Some(50) },
false,
),
],
);
db.create_table(schema).unwrap();
let table = db.get_table_mut("large_table").unwrap();
for i in 0..10000 {
table
.insert(crate::Row::new(vec![
SqlValue::Integer(i as i64),
SqlValue::Varchar(arcstr::ArcStr::from(format!("value_{}", i))),
]))
.unwrap();
}
let path = "/tmp/test_large.sql";
let start = std::time::Instant::now();
db.save_sql_dump(path).unwrap();
let duration = start.elapsed();
assert!(duration.as_secs() < 10, "Export took too long: {:?}", duration);
let metadata = std::fs::metadata(path).unwrap();
assert!(metadata.len() > 0, "File should have content");
let content = std::fs::read_to_string(path).unwrap();
let insert_count = content.matches("INSERT INTO large_table").count();
assert_eq!(insert_count, 10000, "Should have 10,000 INSERT statements");
std::fs::remove_file(path).ok();
}
#[test]
fn test_read_sql_dump() {
use crate::persistence::load::read_sql_dump;
let mut db = Database::new();
let schema = TableSchema::new(
"test_load".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"name".to_string(),
DataType::Varchar { max_length: Some(50) },
false,
),
],
);
db.create_table(schema).unwrap();
let table = db.get_table_mut("test_load").unwrap();
table
.insert(crate::Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("Test")),
]))
.unwrap();
let path = "/tmp/test_read_dump.sql";
db.save_sql_dump(path).unwrap();
let content = read_sql_dump(path).unwrap();
assert!(content.contains("CREATE TABLE"));
assert!(content.contains("INSERT INTO"));
assert!(content.contains("Test"));
std::fs::remove_file(path).ok();
}
#[test]
fn test_read_sql_dump_file_not_found() {
use crate::persistence::load::read_sql_dump;
let result = read_sql_dump("/tmp/nonexistent_file_xyz123.sql");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("does not exist"));
}
#[test]
fn test_sql_dump_preserves_quoted_keyword_table_name() {
use vibesql_ast::TableIdentifier;
let mut db = Database::new();
let schema = TableSchema::new(
"create".to_string(),
vec![ColumnSchema::new("f1".to_string(), DataType::Integer, true)],
);
db.create_table_with_identifier(schema, TableIdentifier::new("create", true)).unwrap();
let path = "/tmp/test_dump_quoted_keyword.sql";
db.save_sql_dump(path).unwrap();
let content = std::fs::read_to_string(path).unwrap();
std::fs::remove_file(path).ok();
assert!(
content.contains("CREATE TABLE \"create\" ("),
"dump should quote the keyword table name verbatim; got:\n{content}"
);
assert!(
!content.contains("CREATE TABLE create ("),
"dump must not emit an unquoted keyword table name; got:\n{content}"
);
}
#[test]
fn test_sql_dump_preserves_mixed_case_table_and_column_names() {
use vibesql_ast::TableIdentifier;
let mut db = Database::new();
let schema = TableSchema::new(
"MixedCase".to_string(),
vec![
ColumnSchema::new("ColA".to_string(), DataType::Integer, true),
ColumnSchema::new("B".to_string(), DataType::Integer, true),
],
);
db.create_table_with_identifier(schema, TableIdentifier::new("MixedCase", true)).unwrap();
let path = "/tmp/test_dump_mixed_case.sql";
db.save_sql_dump(path).unwrap();
let content = std::fs::read_to_string(path).unwrap();
std::fs::remove_file(path).ok();
assert!(
content.contains("CREATE TABLE MixedCase ("),
"dump should echo the original table case; got:\n{content}"
);
assert!(content.contains("ColA"), "dump should echo the original column case; got:\n{content}");
assert!(
!content.contains("CREATE TABLE mixedcase ("),
"dump must not fold the table name to lowercase; got:\n{content}"
);
}