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("stock".to_string(), DataType::Integer, true),
],
vec!["id".to_string()], );
db.create_table(schema).unwrap();
}
fn insert_product(db: &mut Database, id: i64, name: &str, stock: 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(stock)),
]]),
conflict_clause: None,
on_conflict: None,
on_duplicate_key_update: None,
returning: None,
};
InsertExecutor::execute(db, &stmt).unwrap();
}
fn upsert_product(db: &mut Database, id: i64, name: &str, stock: 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(stock)),
]]),
conflict_clause: None,
on_conflict: None,
on_duplicate_key_update: Some(vec![
vibesql_ast::Assignment {
column: "name".to_string(),
value: vibesql_ast::Expression::DuplicateKeyValue { column: "name".to_string() },
},
vibesql_ast::Assignment {
column: "stock".to_string(),
value: vibesql_ast::Expression::DuplicateKeyValue { column: "stock".to_string() },
},
]),
returning: None,
};
InsertExecutor::execute(db, &stmt).unwrap();
}
#[test]
fn test_on_duplicate_key_update_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);
insert_product(&mut db, 3, "Gizmo", 300);
let initial_columnar = db.get_columnar("products").unwrap().expect("Table should exist");
assert_eq!(initial_columnar.row_count(), 3);
let initial_stats = db.columnar_cache_stats();
assert_eq!(initial_stats.conversions, 1, "Should have done one conversion");
let stock_col = initial_columnar.get_column("stock").expect("Column should exist");
let stocks: Vec<i64> = (0..3)
.map(|i| match stock_col.get(i) {
SqlValue::Integer(v) => v,
other => panic!("Expected Integer, got {:?}", other),
})
.collect();
assert_eq!(stocks, vec![100, 200, 300], "Initial stocks should be 100, 200, 300");
upsert_product(&mut db, 1, "Super Widget", 150);
let updated_columnar = db.get_columnar("products").unwrap().expect("Table should exist");
assert_eq!(updated_columnar.row_count(), 3, "Should still have 3 rows");
let updated_stock_col = updated_columnar.get_column("stock").expect("Column should exist");
let updated_stocks: Vec<i64> = (0..3)
.map(|i| match updated_stock_col.get(i) {
SqlValue::Integer(v) => v,
other => panic!("Expected Integer, got {:?}", other),
})
.collect();
assert!(
!updated_stocks.contains(&100),
"Old stock 100 should no longer exist after ON DUPLICATE KEY UPDATE"
);
assert!(
updated_stocks.contains(&150),
"New stock 150 should exist after ON DUPLICATE KEY UPDATE"
);
assert!(updated_stocks.contains(&200), "Unchanged stock 200 should still exist");
assert!(updated_stocks.contains(&300), "Unchanged stock 300 should still exist");
let final_stats = db.columnar_cache_stats();
assert!(
final_stats.conversions >= 2,
"Should have re-converted after ON DUPLICATE KEY UPDATE (conversions: {})",
final_stats.conversions
);
}
#[test]
fn test_on_duplicate_key_update_invalidates_prewarmed_cache() {
let mut db = Database::new();
setup_products_table(&mut db);
insert_product(&mut db, 1, "Item A", 50);
insert_product(&mut db, 2, "Item B", 75);
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(), 2);
let stats_after_get = db.columnar_cache_stats();
assert_eq!(stats_after_get.hits, 1, "Second get should be a cache hit");
upsert_product(&mut db, 1, "Item A Updated", 999);
let updated_columnar = db.get_columnar("products").unwrap().expect("Table should exist");
assert_eq!(updated_columnar.row_count(), 2, "Should still have 2 rows");
let stock_col = updated_columnar.get_column("stock").expect("Column should exist");
let has_new_stock = (0..2).any(|i| matches!(stock_col.get(i), SqlValue::Integer(999)));
assert!(has_new_stock, "New stock 999 should be visible after ON DUPLICATE KEY UPDATE");
let has_old_stock = (0..2).any(|i| matches!(stock_col.get(i), SqlValue::Integer(50)));
assert!(!has_old_stock, "Old stock 50 should NOT be visible after ON DUPLICATE KEY UPDATE");
}
#[test]
fn test_on_duplicate_key_update_unique_constraint_invalidates_cache() {
let mut db = Database::new();
let schema = TableSchema::with_all_constraints(
"users".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("score".to_string(), DataType::Integer, true),
],
Some(vec!["id".to_string()]), vec![vec!["name".to_string()]], );
db.create_table(schema).unwrap();
let insert = |db: &mut Database, id: i64, name: &str, score: i64| {
let stmt = vibesql_ast::InsertStmt {
with_clause: None,
schema_name: None,
schema_quoted: false,
table_quoted: false,
table_name: "users".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(score)),
]]),
conflict_clause: None,
on_conflict: None,
on_duplicate_key_update: None,
returning: None,
};
InsertExecutor::execute(db, &stmt).unwrap();
};
insert(&mut db, 1, "alice", 100);
insert(&mut db, 2, "bob", 200);
let _ = db.get_columnar("users").unwrap();
let upsert_stmt = vibesql_ast::InsertStmt {
with_clause: None,
schema_name: None,
schema_quoted: false,
table_quoted: false,
table_name: "users".to_string(),
columns: vec![],
source: vibesql_ast::InsertSource::Values(vec![vec![
vibesql_ast::Expression::Literal(SqlValue::Integer(3)), vibesql_ast::Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from("alice"))),
vibesql_ast::Expression::Literal(SqlValue::Integer(999)), ]]),
conflict_clause: None,
on_conflict: None,
on_duplicate_key_update: Some(vec![vibesql_ast::Assignment {
column: "score".to_string(),
value: vibesql_ast::Expression::DuplicateKeyValue { column: "score".to_string() },
}]),
returning: None,
};
InsertExecutor::execute(&mut db, &upsert_stmt).unwrap();
let columnar = db.get_columnar("users").unwrap().expect("Table should exist");
assert_eq!(columnar.row_count(), 2, "Should still have 2 rows");
let score_col = columnar.get_column("score").expect("Column should exist");
let has_new_score = (0..2).any(|i| matches!(score_col.get(i), SqlValue::Integer(999)));
assert!(
has_new_score,
"Alice's new score 999 should be visible after ON DUPLICATE KEY UPDATE on UNIQUE conflict"
);
let has_old_score = (0..2).any(|i| matches!(score_col.get(i), SqlValue::Integer(100)));
assert!(
!has_old_score,
"Alice's old score 100 should NOT be visible after ON DUPLICATE KEY UPDATE"
);
}
#[test]
fn test_on_duplicate_key_update_no_conflict_works_with_cache() {
let mut db = Database::new();
setup_products_table(&mut db);
insert_product(&mut db, 1, "Existing", 100);
let _ = db.get_columnar("products").unwrap();
upsert_product(&mut db, 2, "New Item", 200);
let columnar = db.get_columnar("products").unwrap().expect("Table should exist");
assert_eq!(columnar.row_count(), 2, "Should have 2 rows");
let stock_col = columnar.get_column("stock").expect("Column should exist");
let stocks: Vec<i64> = (0..2)
.filter_map(|i| match stock_col.get(i) {
SqlValue::Integer(v) => Some(v),
_ => None,
})
.collect();
assert!(stocks.contains(&100), "Original stock 100 should exist");
assert!(stocks.contains(&200), "New stock 200 should exist");
}
#[test]
fn test_on_duplicate_key_update_arithmetic_invalidates_cache() {
let mut db = Database::new();
setup_products_table(&mut db);
insert_product(&mut db, 1, "Widget", 10);
let initial_columnar = db.get_columnar("products").unwrap().expect("Table should exist");
let initial_stock_col = initial_columnar.get_column("stock").expect("Column should exist");
assert_eq!(initial_stock_col.get(0), SqlValue::Integer(10));
let upsert_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(1)),
vibesql_ast::Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from("Widget"))),
vibesql_ast::Expression::Literal(SqlValue::Integer(30)),
]]),
conflict_clause: None,
on_conflict: None,
on_duplicate_key_update: Some(vec![vibesql_ast::Assignment {
column: "stock".to_string(),
value: vibesql_ast::Expression::BinaryOp {
op: vibesql_ast::BinaryOperator::Plus,
left: Box::new(vibesql_ast::Expression::ColumnRef(
vibesql_ast::ColumnIdentifier::simple("stock", false),
)),
right: Box::new(vibesql_ast::Expression::DuplicateKeyValue {
column: "stock".to_string(),
}),
},
}]),
returning: None,
};
InsertExecutor::execute(&mut db, &upsert_stmt).unwrap();
let updated_columnar = db.get_columnar("products").unwrap().expect("Table should exist");
let updated_stock_col = updated_columnar.get_column("stock").expect("Column should exist");
assert_eq!(
updated_stock_col.get(0),
SqlValue::Integer(40),
"Stock should be 10 + 30 = 40 after arithmetic ON DUPLICATE KEY UPDATE"
);
}