use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_executor::InsertExecutor;
use vibesql_storage::Database;
use vibesql_types::{DataType, SqlValue};
fn setup_products_table(db: &mut Database) {
let schema = TableSchema::with_primary_key(
"products".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("name".to_string(), DataType::Varchar { max_length: Some(50) }, true),
ColumnSchema::new("price".to_string(), DataType::Integer, true),
],
vec!["id".to_string()], );
db.create_table(schema).unwrap();
}
fn insert_product(db: &mut Database, id: i64, name: &str, price: i64) {
let stmt = vibesql_ast::InsertStmt {
with_clause: None,
schema_name: None,
schema_quoted: false,
table_quoted: false,
table_name: "products".to_string(),
columns: vec![],
source: vibesql_ast::InsertSource::Values(vec![vec![
vibesql_ast::Expression::Literal(SqlValue::Integer(id)),
vibesql_ast::Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from(name))),
vibesql_ast::Expression::Literal(SqlValue::Integer(price)),
]]),
conflict_clause: None,
on_conflict: None,
on_duplicate_key_update: None,
returning: None,
};
InsertExecutor::execute(db, &stmt).unwrap();
}
#[test]
fn test_insert_invalidates_columnar_cache() {
let mut db = Database::new();
setup_products_table(&mut db);
insert_product(&mut db, 1, "Widget", 100);
insert_product(&mut db, 2, "Gadget", 200);
let initial_columnar = db.get_columnar("products").unwrap().expect("Table should exist");
assert_eq!(initial_columnar.row_count(), 2);
let initial_stats = db.columnar_cache_stats();
assert_eq!(initial_stats.conversions, 1, "Should have done one conversion");
let price_col = initial_columnar.get_column("price").expect("Column should exist");
let prices: Vec<i64> = (0..2)
.map(|i| match price_col.get(i) {
SqlValue::Integer(v) => v,
other => panic!("Expected Integer, got {:?}", other),
})
.collect();
assert_eq!(prices, vec![100, 200], "Initial prices should be 100, 200");
insert_product(&mut db, 3, "Gizmo", 300);
let updated_columnar = db.get_columnar("products").unwrap().expect("Table should exist");
assert_eq!(updated_columnar.row_count(), 3, "Should have 3 rows after insert");
let updated_price_col = updated_columnar.get_column("price").expect("Column should exist");
let updated_prices: Vec<i64> = (0..3)
.map(|i| match updated_price_col.get(i) {
SqlValue::Integer(v) => v,
other => panic!("Expected Integer, got {:?}", other),
})
.collect();
assert!(updated_prices.contains(&300), "New price 300 should exist after INSERT");
assert!(updated_prices.contains(&100), "Existing price 100 should still exist");
assert!(updated_prices.contains(&200), "Existing price 200 should still exist");
let final_stats = db.columnar_cache_stats();
assert!(
final_stats.conversions >= 2,
"Should have re-converted after INSERT (conversions: {})",
final_stats.conversions
);
}
#[test]
fn test_insert_invalidates_prewarmed_cache() {
let mut db = Database::new();
setup_products_table(&mut db);
insert_product(&mut db, 1, "Item A", 50);
let warmed = db.pre_warm_columnar_cache(&["products"]).unwrap();
assert_eq!(warmed, 1, "Should warm 1 table");
let initial_columnar = db.get_columnar("products").unwrap().expect("Table should exist");
assert_eq!(initial_columnar.row_count(), 1);
let stats_after_get = db.columnar_cache_stats();
assert_eq!(stats_after_get.hits, 1, "Second get should be a cache hit");
insert_product(&mut db, 2, "Item B", 75);
let updated_columnar = db.get_columnar("products").unwrap().expect("Table should exist");
assert_eq!(updated_columnar.row_count(), 2, "Should have 2 rows");
let price_col = updated_columnar.get_column("price").expect("Column should exist");
let has_new_price = (0..2).any(|i| matches!(price_col.get(i), SqlValue::Integer(75)));
assert!(has_new_price, "New price 75 should be visible after INSERT");
}
#[test]
fn test_multi_row_insert_invalidates_cache() {
let mut db = Database::new();
setup_products_table(&mut db);
insert_product(&mut db, 1, "Initial", 100);
let _ = db.get_columnar("products").unwrap();
let stats_before = db.columnar_cache_stats();
let stmt = vibesql_ast::InsertStmt {
with_clause: None,
schema_name: None,
schema_quoted: false,
table_quoted: false,
table_name: "products".to_string(),
columns: vec![],
source: vibesql_ast::InsertSource::Values(vec![
vec![
vibesql_ast::Expression::Literal(SqlValue::Integer(2)),
vibesql_ast::Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from(
"Product 2",
))),
vibesql_ast::Expression::Literal(SqlValue::Integer(200)),
],
vec![
vibesql_ast::Expression::Literal(SqlValue::Integer(3)),
vibesql_ast::Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from(
"Product 3",
))),
vibesql_ast::Expression::Literal(SqlValue::Integer(300)),
],
vec![
vibesql_ast::Expression::Literal(SqlValue::Integer(4)),
vibesql_ast::Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from(
"Product 4",
))),
vibesql_ast::Expression::Literal(SqlValue::Integer(400)),
],
]),
conflict_clause: None,
on_conflict: None,
on_duplicate_key_update: None,
returning: None,
};
InsertExecutor::execute(&mut db, &stmt).unwrap();
let columnar = db.get_columnar("products").unwrap().expect("Table should exist");
assert_eq!(columnar.row_count(), 4, "Should have 4 rows after multi-row INSERT");
let price_col = columnar.get_column("price").expect("Column should exist");
let prices: Vec<i64> = (0..4)
.filter_map(|i| match price_col.get(i) {
SqlValue::Integer(v) => Some(v),
_ => None,
})
.collect();
assert!(prices.contains(&100), "Original price 100 should exist");
assert!(prices.contains(&200), "New price 200 should exist");
assert!(prices.contains(&300), "New price 300 should exist");
assert!(prices.contains(&400), "New price 400 should exist");
let stats_after = db.columnar_cache_stats();
assert!(
stats_after.conversions > stats_before.conversions,
"Should have re-converted after multi-row INSERT"
);
}
#[test]
fn test_insert_select_invalidates_cache() {
let mut db = Database::new();
let source_schema = TableSchema::new(
"source_products".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("name".to_string(), DataType::Varchar { max_length: Some(50) }, true),
ColumnSchema::new("price".to_string(), DataType::Integer, true),
],
);
db.create_table(source_schema).unwrap();
setup_products_table(&mut db);
let insert_source = |db: &mut Database, id: i64, name: &str, price: i64| {
let stmt = vibesql_ast::InsertStmt {
with_clause: None,
schema_name: None,
schema_quoted: false,
table_quoted: false,
table_name: "source_products".to_string(),
columns: vec![],
source: vibesql_ast::InsertSource::Values(vec![vec![
vibesql_ast::Expression::Literal(SqlValue::Integer(id)),
vibesql_ast::Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from(name))),
vibesql_ast::Expression::Literal(SqlValue::Integer(price)),
]]),
conflict_clause: None,
on_conflict: None,
on_duplicate_key_update: None,
returning: None,
};
InsertExecutor::execute(db, &stmt).unwrap();
};
insert_source(&mut db, 1, "Source Item 1", 111);
insert_source(&mut db, 2, "Source Item 2", 222);
let initial_columnar = db.get_columnar("products").unwrap().expect("Table should exist");
assert_eq!(initial_columnar.row_count(), 0, "Destination should start empty");
let select_stmt = vibesql_ast::SelectStmt {
with_clause: None,
distinct: false,
select_list: vec![vibesql_ast::SelectItem::Wildcard { alias: None }],
into_table: None,
into_variables: None,
from: Some(vibesql_ast::FromClause::Table {
index_hint: None,
name: "source_products".to_string(),
alias: None,
column_aliases: None,
quoted: false,
}),
where_clause: None,
group_by: None,
having: None,
window_definitions: None,
order_by: None,
limit: None,
offset: None,
set_operation: None,
values: None,
};
let insert_stmt = vibesql_ast::InsertStmt {
with_clause: None,
schema_name: None,
schema_quoted: false,
table_quoted: false,
table_name: "products".to_string(),
columns: vec![],
source: vibesql_ast::InsertSource::Select(Box::new(select_stmt)),
conflict_clause: None,
on_conflict: None,
on_duplicate_key_update: None,
returning: None,
};
InsertExecutor::execute(&mut db, &insert_stmt).unwrap();
let updated_columnar = db.get_columnar("products").unwrap().expect("Table should exist");
assert_eq!(updated_columnar.row_count(), 2, "Should have 2 rows after INSERT SELECT");
let price_col = updated_columnar.get_column("price").expect("Column should exist");
let has_price_111 = (0..2).any(|i| matches!(price_col.get(i), SqlValue::Integer(111)));
let has_price_222 = (0..2).any(|i| matches!(price_col.get(i), SqlValue::Integer(222)));
assert!(has_price_111, "Price 111 from source should be visible");
assert!(has_price_222, "Price 222 from source should be visible");
}