use vibesql_catalog::{ColumnSchema, TableIdentifier, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::Database;
#[test]
fn test_quoted_table_identifier_roundtrip() {
let mut db = Database::new();
let schema = TableSchema::new(
"MyTable".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
let identifier = TableIdentifier::quoted("MyTable");
db.create_table_with_identifier(schema, identifier).unwrap();
let original_identifier = db.catalog.get_table_identifier("mytable").unwrap();
assert!(original_identifier.is_quoted(), "Original table should be quoted");
assert_eq!(original_identifier.canonical(), "mytable");
let path = "/tmp/test_quoted_identifier.vbsql";
db.save_binary(path).unwrap();
let loaded_db = Database::load_binary(path).unwrap();
let loaded_identifier = loaded_db.catalog.get_table_identifier("mytable").unwrap();
assert!(loaded_identifier.is_quoted(), "Loaded table should still be quoted after roundtrip");
assert_eq!(loaded_identifier.canonical(), "mytable");
std::fs::remove_file(path).ok();
}
#[test]
fn test_binary_file_roundtrip_preserves_verbatim_sql_source() {
let mut db = Database::new();
let original_sql = "CREATE TABLE t1(\n a INTEGER,\n b TEXT\n)";
let mut schema = TableSchema::new(
"t1".to_string(),
vec![
ColumnSchema::new("a".to_string(), DataType::Integer, true),
ColumnSchema::new("b".to_string(), DataType::Varchar { max_length: None }, true),
],
);
schema.set_sql_source(original_sql);
db.create_table_with_identifier(schema, TableIdentifier::new("t1", false)).unwrap();
db.get_table_mut("t1")
.unwrap()
.insert(crate::Row::new(vec![SqlValue::Integer(1), SqlValue::Varchar("x".into())]))
.unwrap();
let path = "/tmp/test_sql_source_binary_roundtrip.vbsql";
db.save_binary(path).unwrap();
let loaded_db = Database::load_binary(path).unwrap();
let table = loaded_db.get_table("t1").expect("t1 must survive the binary file round-trip");
assert_eq!(
table.schema.sql_source.as_deref(),
Some(original_sql),
"verbatim multi-line CREATE TABLE source must survive a save_binary/load_binary cycle"
);
assert_eq!(table.row_count(), 1);
std::fs::remove_file(path).ok();
}
#[test]
fn test_unquoted_table_identifier_roundtrip() {
let mut db = Database::new();
let schema = TableSchema::new(
"users".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
let identifier = TableIdentifier::unquoted("users");
db.create_table_with_identifier(schema, identifier).unwrap();
let original_identifier = db.catalog.get_table_identifier("users").unwrap();
assert!(!original_identifier.is_quoted(), "Original table should be unquoted");
assert_eq!(original_identifier.canonical(), "users");
let path = "/tmp/test_unquoted_identifier.vbsql";
db.save_binary(path).unwrap();
let loaded_db = Database::load_binary(path).unwrap();
let loaded_identifier = loaded_db.catalog.get_table_identifier("users").unwrap();
assert!(
!loaded_identifier.is_quoted(),
"Loaded table should still be unquoted after roundtrip"
);
assert_eq!(loaded_identifier.canonical(), "users");
std::fs::remove_file(path).ok();
}
#[test]
fn test_mixed_quoted_unquoted_tables_roundtrip() {
let mut db = Database::new();
let schema1 = TableSchema::new(
"users".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table_with_identifier(schema1, TableIdentifier::unquoted("users")).unwrap();
let schema2 = TableSchema::new(
"UserProfiles".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table_with_identifier(schema2, TableIdentifier::quoted("UserProfiles")).unwrap();
let path = "/tmp/test_mixed_identifiers.vbsql";
db.save_binary(path).unwrap();
let loaded_db = Database::load_binary(path).unwrap();
let users_id = loaded_db.catalog.get_table_identifier("users").unwrap();
assert!(!users_id.is_quoted(), "users should remain unquoted");
let profiles_id = loaded_db.catalog.get_table_identifier("userprofiles").unwrap();
assert!(profiles_id.is_quoted(), "UserProfiles should remain quoted");
std::fs::remove_file(path).ok();
}
#[test]
fn test_quoted_table_with_data_roundtrip() {
let mut db = Database::new();
let schema = TableSchema::new(
"CaseSensitiveTable".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"name".to_string(),
DataType::Varchar { max_length: Some(100) },
true,
),
],
);
db.create_table_with_identifier(schema, TableIdentifier::quoted("CaseSensitiveTable")).unwrap();
let table = db.get_table_mut("CaseSensitiveTable").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_quoted_with_data.vbsql";
db.save_binary(path).unwrap();
let loaded_db = Database::load_binary(path).unwrap();
let identifier = loaded_db.catalog.get_table_identifier("casesensitivetable").unwrap();
assert!(identifier.is_quoted());
let loaded_table = loaded_db.get_table("CaseSensitiveTable").unwrap();
assert_eq!(loaded_table.row_count(), 2);
std::fs::remove_file(path).ok();
}
#[test]
fn test_legacy_table_creation_defaults_to_unquoted() {
let mut db = Database::new();
let schema = TableSchema::new(
"legacy_table".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
let identifier = db.catalog.get_table_identifier("legacy_table").unwrap();
assert!(!identifier.is_quoted(), "Legacy tables should default to unquoted");
let path = "/tmp/test_legacy_table.vbsql";
db.save_binary(path).unwrap();
let loaded_db = Database::load_binary(path).unwrap();
let loaded_id = loaded_db.catalog.get_table_identifier("legacy_table").unwrap();
assert!(!loaded_id.is_quoted());
std::fs::remove_file(path).ok();
}
#[test]
fn test_mvcc_version_fields_roundtrip_v7() {
let mut db = Database::new();
let schema = TableSchema::new(
"mvcc_versioned".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
let table = db.get_table_mut("mvcc_versioned").unwrap();
{
let mut row = crate::Row::from_vec(vec![SqlValue::Integer(1)]);
row.xmin = 100;
row.xmax = None; table.insert(row).unwrap();
}
{
let mut row = crate::Row::from_vec(vec![SqlValue::Integer(2)]);
row.xmin = 200;
row.xmax = Some(300); table.insert(row).unwrap();
}
{
let mut row = crate::Row::from_vec(vec![SqlValue::Integer(3)]);
row.xmin = u64::MAX - 1; row.xmax = Some(u64::MAX);
table.insert(row).unwrap();
}
let path = "/tmp/test_mvcc_roundtrip_v7.vbsql";
db.save_binary(path).unwrap();
let loaded_db = Database::load_binary(path).unwrap();
let loaded_table = loaded_db.get_table("mvcc_versioned").unwrap();
assert_eq!(loaded_table.row_count(), 3);
let rows: Vec<&crate::Row> = loaded_table.scan().iter().collect();
let by_id = |id: i64| -> &crate::Row {
rows.iter()
.copied()
.find(|r| matches!(r.values[0], SqlValue::Integer(v) if v == id))
.unwrap_or_else(|| panic!("row with id={} not found after load", id))
};
let r1 = by_id(1);
assert_eq!(r1.xmin, 100);
assert_eq!(r1.xmax, None);
let r2 = by_id(2);
assert_eq!(r2.xmin, 200);
assert_eq!(r2.xmax, Some(300));
let r3 = by_id(3);
assert_eq!(r3.xmin, u64::MAX - 1);
assert_eq!(r3.xmax, Some(u64::MAX));
std::fs::remove_file(path).ok();
}
#[test]
fn test_default_rows_carry_pre_mvcc_sentinel() {
use crate::row::PRE_MVCC_TXN_ID;
let mut db = Database::new();
let schema = TableSchema::new(
"sentinel_default".to_string(),
vec![ColumnSchema::new("n".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
let table = db.get_table_mut("sentinel_default").unwrap();
table.insert(crate::Row::new(vec![SqlValue::Integer(7)])).unwrap();
table.insert(crate::Row::from_vec(vec![SqlValue::Integer(8)])).unwrap();
let path = "/tmp/test_default_sentinel.vbsql";
db.save_binary(path).unwrap();
let loaded_db = Database::load_binary(path).unwrap();
let loaded_table = loaded_db.get_table("sentinel_default").unwrap();
for row in loaded_table.scan() {
assert_eq!(row.xmin, PRE_MVCC_TXN_ID);
assert_eq!(row.xmax, None);
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_v6_to_v7_read_compatibility_via_synthesized_v6_file() {
use crate::row::PRE_MVCC_TXN_ID;
let mut db = Database::new();
let schema = TableSchema::new(
"v6compat".to_string(),
vec![ColumnSchema::new("n".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
let table = db.get_table_mut("v6compat").unwrap();
table.insert(crate::Row::new(vec![SqlValue::Integer(11)])).unwrap();
table.insert(crate::Row::new(vec![SqlValue::Integer(22)])).unwrap();
let v7_path = "/tmp/test_v6_compat_source.vbsql";
db.save_binary(v7_path).unwrap();
let v7_bytes = std::fs::read(v7_path).unwrap();
std::fs::remove_file(v7_path).ok();
let mut v6_bytes: Vec<u8> = Vec::new();
{
crate::persistence::binary::write_header(&mut v6_bytes).unwrap();
v6_bytes[5] = 6;
crate::persistence::binary::catalog::write_catalog(&mut v6_bytes, &db).unwrap();
let table_names = db.catalog.list_tables();
for table_name in table_names {
if let Some(table) = db.get_table(&table_name) {
crate::persistence::binary::io::write_string(&mut v6_bytes, &table_name).unwrap();
crate::persistence::binary::io::write_u64(&mut v6_bytes, table.row_count() as u64)
.unwrap();
for (_idx, row) in table.scan_live() {
for value in &row.values {
crate::persistence::binary::value::write_sql_value(&mut v6_bytes, value)
.unwrap();
}
}
}
}
let _ = v7_bytes; }
let mut reader = &v6_bytes[..];
let header_version = crate::persistence::binary::read_header(&mut reader).unwrap();
assert_eq!(header_version, 6, "synthetic file claims v6 in its header");
let mut loaded_db = crate::persistence::binary::read_catalog_v(
&mut reader,
crate::persistence::binary::format::VERSION,
)
.unwrap();
crate::persistence::binary::read_data(&mut reader, &mut loaded_db, 6).unwrap();
let loaded_table = loaded_db.get_table("v6compat").unwrap();
assert_eq!(loaded_table.row_count(), 2);
for row in loaded_table.scan() {
assert_eq!(
row.xmin, PRE_MVCC_TXN_ID,
"v6 row should be stamped with PRE_MVCC_TXN_ID after upgrade-read"
);
assert_eq!(row.xmax, None, "v6 row should have no xmax");
}
let mut found = std::collections::HashSet::new();
for row in loaded_table.scan() {
if let SqlValue::Integer(v) = row.values[0] {
found.insert(v);
}
}
assert!(found.contains(&11));
assert!(found.contains(&22));
}
#[test]
fn test_format_version_is_at_least_7() {
assert!(
crate::persistence::binary::format::VERSION >= 7,
"binary format VERSION must be >= 7 after Phase 1a of #5136 (got {})",
crate::persistence::binary::format::VERSION
);
}
#[test]
fn test_partial_index_where_clause_written_to_binary_v8() {
use vibesql_parser::arena_parser::parse_expression_to_owned;
let mut db = Database::new();
let schema = TableSchema::new(
"p1".to_string(),
vec![
ColumnSchema::new("x".to_string(), DataType::Integer, true),
ColumnSchema::new("y".to_string(), DataType::Integer, true),
],
);
db.create_table(schema).unwrap();
let ast_columns = vec![vibesql_ast::IndexColumn::new_column(
"x".to_string(),
vibesql_ast::OrderDirection::Asc,
)];
db.create_index("p1x".to_string(), "p1".to_string(), true, ast_columns).unwrap();
let catalog_meta = vibesql_catalog::IndexMetadata::new(
"p1x".to_string(),
"p1".to_string(),
vibesql_catalog::IndexType::BTree,
vec![vibesql_catalog::IndexedColumn::new_column(
"x".to_string(),
vibesql_catalog::SortOrder::Ascending,
)],
true,
);
db.catalog.add_index(catalog_meta).unwrap();
let predicate_expr = parse_expression_to_owned("y < 2").unwrap();
let updated = db.catalog.set_index_where_clause("p1x", Some(predicate_expr));
assert!(updated, "set_index_where_clause should find p1x");
let path = "/tmp/test_partial_index_v8_write.vbsql";
db.save_binary(path).unwrap();
let bytes = std::fs::read(path).unwrap();
std::fs::remove_file(path).ok();
let as_str = String::from_utf8_lossy(&bytes);
assert!(
as_str.contains("y") && as_str.contains("<") && as_str.contains("2"),
"v8 binary should contain the partial-index WHERE expression text"
);
assert!(
crate::persistence::binary::format::VERSION >= 8,
"binary format VERSION must be >= 8 for partial-index WHERE support (got {})",
crate::persistence::binary::format::VERSION
);
}
#[test]
fn test_partial_index_sql_dump_emits_where_clause() {
use vibesql_parser::arena_parser::parse_expression_to_owned;
let mut db = Database::new();
let schema = TableSchema::new(
"p1".to_string(),
vec![
ColumnSchema::new("x".to_string(), DataType::Integer, true),
ColumnSchema::new("y".to_string(), DataType::Integer, true),
],
);
db.create_table(schema).unwrap();
let ast_columns = vec![vibesql_ast::IndexColumn::new_column(
"x".to_string(),
vibesql_ast::OrderDirection::Asc,
)];
db.create_index("p1x".to_string(), "p1".to_string(), true, ast_columns).unwrap();
let catalog_meta = vibesql_catalog::IndexMetadata::new(
"p1x".to_string(),
"p1".to_string(),
vibesql_catalog::IndexType::BTree,
vec![vibesql_catalog::IndexedColumn::new_column(
"x".to_string(),
vibesql_catalog::SortOrder::Ascending,
)],
true,
);
db.catalog.add_index(catalog_meta).unwrap();
let predicate_expr = parse_expression_to_owned("y < 2").unwrap();
db.catalog.set_index_where_clause("p1x", Some(predicate_expr));
let path = "/tmp/test_partial_index_dump_emit.sql";
db.save_sql_dump(path).unwrap();
let dump = std::fs::read_to_string(path).unwrap();
std::fs::remove_file(path).ok();
assert!(
dump.contains("CREATE UNIQUE INDEX") && dump.contains("WHERE"),
"SQL dump must emit WHERE for partial index; dump was:\n{}",
dump
);
}
#[test]
fn test_partial_index_round_trips_through_binary_load() {
use vibesql_parser::arena_parser::parse_expression_to_owned;
let mut db = Database::new();
let schema = TableSchema::new(
"p1".to_string(),
vec![
ColumnSchema::new("x".to_string(), DataType::Integer, true),
ColumnSchema::new("y".to_string(), DataType::Integer, true),
],
);
db.create_table(schema).unwrap();
let ast_columns = vec![vibesql_ast::IndexColumn::new_column(
"x".to_string(),
vibesql_ast::OrderDirection::Asc,
)];
db.create_index("p1x".to_string(), "p1".to_string(), true, ast_columns).unwrap();
let catalog_meta = vibesql_catalog::IndexMetadata::new(
"p1x".to_string(),
"p1".to_string(),
vibesql_catalog::IndexType::BTree,
vec![vibesql_catalog::IndexedColumn::new_column(
"x".to_string(),
vibesql_catalog::SortOrder::Ascending,
)],
true,
);
db.catalog.add_index(catalog_meta).unwrap();
let predicate_expr = parse_expression_to_owned("y < 2").unwrap();
let updated = db.catalog.set_index_where_clause("p1x", Some(predicate_expr));
assert!(updated, "set_index_where_clause should find p1x in the source catalog");
let src_meta = db.catalog.find_index_by_name("p1x").expect("p1x in source catalog");
assert!(src_meta.is_partial(), "source catalog should report p1x as partial");
let path = "/tmp/test_partial_index_v8_roundtrip.vbsql";
db.save_binary(path).unwrap();
let loaded = Database::load_binary(path).unwrap();
std::fs::remove_file(path).ok();
let loaded_meta = loaded
.catalog
.find_index_by_name("p1x")
.expect("loaded catalog must repopulate IndexMetadata for persisted indexes (#5215)");
assert!(
loaded_meta.is_partial(),
"loaded catalog must preserve partial-index WHERE clause (#5215)"
);
assert_eq!(loaded_meta.name, "p1x");
assert_eq!(loaded_meta.table_name, "p1");
assert!(loaded_meta.is_unique);
}
#[test]
fn test_load_binary_repopulates_catalog_for_all_indexes() {
let mut db = Database::new();
let schema = TableSchema::new(
"t".to_string(),
vec![
ColumnSchema::new("a".to_string(), DataType::Integer, true),
ColumnSchema::new("b".to_string(), DataType::Integer, true),
],
);
db.create_table(schema).unwrap();
db.create_index(
"idx_a".to_string(),
"t".to_string(),
false,
vec![vibesql_ast::IndexColumn::new_column(
"a".to_string(),
vibesql_ast::OrderDirection::Asc,
)],
)
.unwrap();
db.catalog
.add_index(vibesql_catalog::IndexMetadata::new(
"idx_a".to_string(),
"t".to_string(),
vibesql_catalog::IndexType::BTree,
vec![vibesql_catalog::IndexedColumn::new_column(
"a".to_string(),
vibesql_catalog::SortOrder::Ascending,
)],
false,
))
.unwrap();
db.create_index(
"idx_b_unique".to_string(),
"t".to_string(),
true,
vec![vibesql_ast::IndexColumn::new_column(
"b".to_string(),
vibesql_ast::OrderDirection::Asc,
)],
)
.unwrap();
db.catalog
.add_index(vibesql_catalog::IndexMetadata::new(
"idx_b_unique".to_string(),
"t".to_string(),
vibesql_catalog::IndexType::BTree,
vec![vibesql_catalog::IndexedColumn::new_column(
"b".to_string(),
vibesql_catalog::SortOrder::Ascending,
)],
true,
))
.unwrap();
let path = "/tmp/test_repopulate_catalog_indexes.vbsql";
db.save_binary(path).unwrap();
let loaded = Database::load_binary(path).unwrap();
std::fs::remove_file(path).ok();
let m1 =
loaded.catalog.find_index_by_name("idx_a").expect("idx_a must repopulate after load");
assert!(!m1.is_unique);
assert!(!m1.is_partial());
assert_eq!(m1.table_name, "t");
let m2 = loaded
.catalog
.find_index_by_name("idx_b_unique")
.expect("idx_b_unique must repopulate after load");
assert!(m2.is_unique);
assert!(!m2.is_partial());
assert_eq!(m2.table_name, "t");
}