use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_parser::Parser;
use vibesql_storage::{Database, Row};
use vibesql_types::{DataType, SqlValue};
use crate::errors::ExecutorError;
fn create_test_db() -> Database {
let mut db = Database::new();
db.catalog.set_case_sensitive_identifiers(false);
let users_schema = TableSchema::new(
"users".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"email".to_string(),
DataType::Varchar { max_length: Some(100) },
true, ),
ColumnSchema::new(
"first_name".to_string(),
DataType::Varchar { max_length: Some(50) },
true,
),
ColumnSchema::new(
"last_name".to_string(),
DataType::Varchar { max_length: Some(50) },
true,
),
ColumnSchema::new(
"phone".to_string(),
DataType::Varchar { max_length: Some(20) },
true,
),
],
);
db.create_table(users_schema).unwrap();
db
}
#[test]
fn test_unique_index_basic_insert_enforcement() {
let mut db = create_test_db();
db.create_index(
"idx_users_email".to_string(),
"users".to_string(),
true, vec![IndexColumn::Column {
column_name: "email".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
}],
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("alice@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Alice")),
SqlValue::Varchar(arcstr::ArcStr::from("Smith")),
SqlValue::Null,
]),
)
.unwrap();
let result = db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(2),
SqlValue::Varchar(arcstr::ArcStr::from("alice@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Bob")),
SqlValue::Varchar(arcstr::ArcStr::from("Jones")),
SqlValue::Null,
]),
);
assert!(result.is_err());
match result.unwrap_err() {
vibesql_storage::StorageError::UniqueConstraintViolation(msg) => {
assert!(msg.contains("UNIQUE constraint"));
assert!(
msg.contains("users.email"),
"Expected message to contain 'users.email', got: {}",
msg
);
}
e => panic!("Expected UniqueConstraintViolation, got {:?}", e),
}
}
#[test]
fn test_unique_index_null_values_allowed() {
let mut db = create_test_db();
db.create_index(
"idx_users_email".to_string(),
"users".to_string(),
true, vec![IndexColumn::Column {
column_name: "email".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
}],
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(1),
SqlValue::Null, SqlValue::Varchar(arcstr::ArcStr::from("Alice")),
SqlValue::Varchar(arcstr::ArcStr::from("Smith")),
SqlValue::Null,
]),
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(2),
SqlValue::Null, SqlValue::Varchar(arcstr::ArcStr::from("Bob")),
SqlValue::Varchar(arcstr::ArcStr::from("Jones")),
SqlValue::Null,
]),
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(3),
SqlValue::Null, SqlValue::Varchar(arcstr::ArcStr::from("Charlie")),
SqlValue::Varchar(arcstr::ArcStr::from("Brown")),
SqlValue::Null,
]),
)
.unwrap();
let table = db.get_table("users").unwrap();
assert_eq!(table.scan().len(), 3);
}
#[test]
fn test_unique_index_composite_key() {
let mut db = create_test_db();
db.create_index(
"idx_users_name".to_string(),
"users".to_string(),
true, vec![
IndexColumn::Column {
column_name: "first_name".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
},
IndexColumn::Column {
column_name: "last_name".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
},
],
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("john@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("John")),
SqlValue::Varchar(arcstr::ArcStr::from("Doe")),
SqlValue::Null,
]),
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(2),
SqlValue::Varchar(arcstr::ArcStr::from("johnsmith@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("John")),
SqlValue::Varchar(arcstr::ArcStr::from("Smith")),
SqlValue::Null,
]),
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(3),
SqlValue::Varchar(arcstr::ArcStr::from("jane@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Jane")),
SqlValue::Varchar(arcstr::ArcStr::from("Doe")),
SqlValue::Null,
]),
)
.unwrap();
let result = db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(4),
SqlValue::Varchar(arcstr::ArcStr::from("johndoe2@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("John")),
SqlValue::Varchar(arcstr::ArcStr::from("Doe")),
SqlValue::Null,
]),
);
assert!(result.is_err());
match result.unwrap_err() {
vibesql_storage::StorageError::UniqueConstraintViolation(msg) => {
assert!(msg.contains("UNIQUE constraint"));
assert!(
msg.contains("users.first_name") && msg.contains("users.last_name"),
"Expected message to contain 'users.first_name' and 'users.last_name', got: {}",
msg
);
}
e => panic!("Expected UniqueConstraintViolation, got {:?}", e),
}
}
#[test]
fn test_unique_index_composite_with_null() {
let mut db = create_test_db();
db.create_index(
"idx_users_name".to_string(),
"users".to_string(),
true, vec![
IndexColumn::Column {
column_name: "first_name".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
},
IndexColumn::Column {
column_name: "last_name".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
},
],
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("test1@example.com")),
SqlValue::Null,
SqlValue::Varchar(arcstr::ArcStr::from("Doe")),
SqlValue::Null,
]),
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(2),
SqlValue::Varchar(arcstr::ArcStr::from("test2@example.com")),
SqlValue::Null,
SqlValue::Varchar(arcstr::ArcStr::from("Doe")),
SqlValue::Null,
]),
)
.unwrap();
let table = db.get_table("users").unwrap();
assert_eq!(table.scan().len(), 2);
}
#[test]
fn test_unique_index_update_enforcement() {
let mut db = create_test_db();
db.create_index(
"idx_users_email".to_string(),
"users".to_string(),
true, vec![IndexColumn::Column {
column_name: "email".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
}],
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("alice@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Alice")),
SqlValue::Varchar(arcstr::ArcStr::from("Smith")),
SqlValue::Null,
]),
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(2),
SqlValue::Varchar(arcstr::ArcStr::from("bob@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Bob")),
SqlValue::Varchar(arcstr::ArcStr::from("Jones")),
SqlValue::Null,
]),
)
.unwrap();
let parsed =
Parser::parse_sql("UPDATE users SET email = 'alice@example.com' WHERE id = 2").unwrap();
let stmt = match parsed {
vibesql_ast::Statement::Update(stmt) => stmt,
_ => panic!("Expected UPDATE statement"),
};
let result = crate::update::UpdateExecutor::execute(&stmt, &mut db);
assert!(result.is_err());
match result.unwrap_err() {
ExecutorError::ConstraintViolation(msg) => {
eprintln!("Error message: {}", msg);
assert!(msg.contains("UNIQUE constraint"), "Message: {}", msg);
assert!(
msg.contains("users.email"),
"Expected message to contain 'users.email', got: {}",
msg
);
}
e => panic!("Expected ConstraintViolation, got {:?}", e),
}
}
#[test]
fn test_unique_index_update_same_value_allowed() {
let mut db = create_test_db();
db.create_index(
"idx_users_email".to_string(),
"users".to_string(),
true, vec![IndexColumn::Column {
column_name: "email".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
}],
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("alice@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Alice")),
SqlValue::Varchar(arcstr::ArcStr::from("Smith")),
SqlValue::Null,
]),
)
.unwrap();
let parsed = Parser::parse_sql(
"UPDATE users SET email = 'alice@example.com', first_name = 'Alicia' WHERE id = 1",
)
.unwrap();
let stmt = match parsed {
vibesql_ast::Statement::Update(stmt) => stmt,
_ => panic!("Expected UPDATE statement"),
};
let result = crate::update::UpdateExecutor::execute(&stmt, &mut db);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1); }
#[test]
fn test_unique_index_update_to_different_value() {
let mut db = create_test_db();
db.create_index(
"idx_users_email".to_string(),
"users".to_string(),
true, vec![IndexColumn::Column {
column_name: "email".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
}],
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("alice@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Alice")),
SqlValue::Varchar(arcstr::ArcStr::from("Smith")),
SqlValue::Null,
]),
)
.unwrap();
let parsed =
Parser::parse_sql("UPDATE users SET email = 'newemail@example.com' WHERE id = 1").unwrap();
let stmt = match parsed {
vibesql_ast::Statement::Update(stmt) => stmt,
_ => panic!("Expected UPDATE statement"),
};
let result = crate::update::UpdateExecutor::execute(&stmt, &mut db);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
let table = db.get_table("users").unwrap();
let row = &table.scan()[0];
assert_eq!(row.values[1], SqlValue::Varchar(arcstr::ArcStr::from("newemail@example.com")));
}
#[test]
fn test_unique_index_update_to_null() {
let mut db = create_test_db();
db.create_index(
"idx_users_email".to_string(),
"users".to_string(),
true, vec![IndexColumn::Column {
column_name: "email".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
}],
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("alice@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Alice")),
SqlValue::Varchar(arcstr::ArcStr::from("Smith")),
SqlValue::Null,
]),
)
.unwrap();
let parsed = Parser::parse_sql("UPDATE users SET email = NULL WHERE id = 1").unwrap();
let stmt = match parsed {
vibesql_ast::Statement::Update(stmt) => stmt,
_ => panic!("Expected UPDATE statement"),
};
let result = crate::update::UpdateExecutor::execute(&stmt, &mut db);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
let table = db.get_table("users").unwrap();
let row = &table.scan()[0];
assert_eq!(row.values[1], SqlValue::Null);
}
#[test]
fn test_unique_index_multiple_indexes_on_table() {
let mut db = create_test_db();
db.create_index(
"idx_users_email".to_string(),
"users".to_string(),
true, vec![IndexColumn::Column {
column_name: "email".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
}],
)
.unwrap();
db.create_index(
"idx_users_phone".to_string(),
"users".to_string(),
true, vec![IndexColumn::Column {
column_name: "phone".to_string(),
prefix_length: None,
direction: OrderDirection::Asc,
}],
)
.unwrap();
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("alice@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Alice")),
SqlValue::Varchar(arcstr::ArcStr::from("Smith")),
SqlValue::Varchar(arcstr::ArcStr::from("555-0001")),
]),
)
.unwrap();
let result1 = db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(2),
SqlValue::Varchar(arcstr::ArcStr::from("alice@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Bob")),
SqlValue::Varchar(arcstr::ArcStr::from("Jones")),
SqlValue::Varchar(arcstr::ArcStr::from("555-0002")),
]),
);
assert!(result1.is_err());
let result2 = db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(3),
SqlValue::Varchar(arcstr::ArcStr::from("charlie@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Charlie")),
SqlValue::Varchar(arcstr::ArcStr::from("Brown")),
SqlValue::Varchar(arcstr::ArcStr::from("555-0001")),
]),
);
assert!(result2.is_err());
db.insert_row(
"users",
Row::new(vec![
SqlValue::Integer(4),
SqlValue::Varchar(arcstr::ArcStr::from("diana@example.com")),
SqlValue::Varchar(arcstr::ArcStr::from("Diana")),
SqlValue::Varchar(arcstr::ArcStr::from("Prince")),
SqlValue::Varchar(arcstr::ArcStr::from("555-0003")),
]),
)
.unwrap();
let table = db.get_table("users").unwrap();
assert_eq!(table.scan().len(), 2); }