use std::collections::BTreeMap;
use vibesql_executor::{
CreateIndexExecutor, CreateTableExecutor, DeleteExecutor, InsertExecutor, UpdateExecutor,
};
use vibesql_parser::Parser;
use vibesql_storage::{database::indexes::IndexData, Database};
use vibesql_types::SqlValue;
fn execute_sql(db: &mut Database, sql: &str) {
for sql_stmt in sql.split(';') {
let trimmed = sql_stmt.trim();
if trimmed.is_empty() {
continue;
}
let stmt = Parser::parse_sql(trimmed).expect("Failed to parse SQL");
execute_statement(&stmt, db);
}
}
fn execute_statement(stmt: &vibesql_ast::Statement, db: &mut Database) {
use vibesql_ast::Statement;
match stmt {
Statement::CreateTable(s) => {
CreateTableExecutor::execute(s, db).expect("CREATE TABLE failed");
}
Statement::CreateIndex(s) => {
CreateIndexExecutor::execute(s, db).expect("CREATE INDEX failed");
}
Statement::Insert(s) => {
InsertExecutor::execute(db, s).expect("INSERT failed");
}
Statement::Update(s) => {
UpdateExecutor::execute(s, db).expect("UPDATE failed");
}
Statement::Delete(s) => {
DeleteExecutor::execute(s, db).expect("DELETE failed");
}
_ => panic!("Unsupported statement type"),
}
}
fn index_body(db: &Database, index_name: &str) -> BTreeMap<Vec<SqlValue>, Vec<usize>> {
let data = db.get_index_data(index_name).expect("index not found");
match data {
IndexData::InMemory { data, .. } => {
data.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
}
_ => panic!("expected InMemory index body for this test"),
}
}
fn index_row_indices(db: &Database, index_name: &str) -> Vec<usize> {
let mut all: Vec<usize> = index_body(db, index_name).into_values().flatten().collect();
all.sort_unstable();
all
}
#[test]
fn create_partial_index_excludes_non_matching_rows_at_build_time() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE orders (id INTEGER PRIMARY KEY, status INTEGER);
INSERT INTO orders VALUES (1, 0);
INSERT INTO orders VALUES (2, 1);
INSERT INTO orders VALUES (3, 1);
INSERT INTO orders VALUES (4, 0);
CREATE INDEX idx_open ON orders(id) WHERE status = 1;
"#,
);
let entries = index_row_indices(&db, "idx_open");
assert_eq!(entries, vec![1, 2], "partial index body should exclude non-matching rows");
}
#[test]
fn insert_into_partial_index_evaluates_predicate() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE orders (id INTEGER PRIMARY KEY, status INTEGER);
CREATE INDEX idx_open ON orders(id) WHERE status = 1;
"#,
);
assert!(index_body(&db, "idx_open").is_empty());
execute_sql(
&mut db,
r#"
INSERT INTO orders VALUES (1, 0);
INSERT INTO orders VALUES (2, 1);
INSERT INTO orders VALUES (3, 0);
INSERT INTO orders VALUES (4, 1);
"#,
);
let entries = index_row_indices(&db, "idx_open");
assert_eq!(entries, vec![1, 3], "only status=1 rows should be in the partial index");
}
#[test]
fn update_partial_index_handles_predicate_transition() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE orders (id INTEGER PRIMARY KEY, status INTEGER);
INSERT INTO orders VALUES (1, 0);
INSERT INTO orders VALUES (2, 1);
INSERT INTO orders VALUES (3, 0);
CREATE INDEX idx_open ON orders(id) WHERE status = 1;
"#,
);
assert_eq!(index_row_indices(&db, "idx_open"), vec![1]);
execute_sql(&mut db, "UPDATE orders SET status = 1 WHERE id = 1");
assert_eq!(index_row_indices(&db, "idx_open"), vec![0, 1]);
execute_sql(&mut db, "UPDATE orders SET status = 0 WHERE id = 2");
assert_eq!(index_row_indices(&db, "idx_open"), vec![0]);
execute_sql(&mut db, "UPDATE orders SET id = 99 WHERE id = 1");
let body = index_body(&db, "idx_open");
assert_eq!(body.len(), 1);
let only_key = body.keys().next().unwrap();
let key_is_99 = match only_key.first() {
Some(SqlValue::Integer(n)) => *n == 99,
Some(SqlValue::Bigint(n)) => *n == 99,
Some(SqlValue::Smallint(n)) => *n == 99,
Some(SqlValue::Double(n)) => (*n - 99.0).abs() < 1e-9,
Some(SqlValue::Real(n)) => (*n - 99.0).abs() < 1e-9,
Some(SqlValue::Float(n)) => (*n - 99.0).abs() < 1e-9,
_ => false,
};
assert!(
key_is_99,
"after key change, partial-index entry should be re-keyed to 99 (saw {:?})",
only_key
);
}
#[test]
fn delete_removes_partial_index_entry_when_predicate_was_truthy() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE orders (id INTEGER PRIMARY KEY, status INTEGER);
INSERT INTO orders VALUES (1, 1);
INSERT INTO orders VALUES (2, 0);
INSERT INTO orders VALUES (3, 1);
CREATE INDEX idx_open ON orders(id) WHERE status = 1;
"#,
);
assert_eq!(index_row_indices(&db, "idx_open"), vec![0, 2]);
execute_sql(&mut db, "DELETE FROM orders WHERE id = 1");
assert_eq!(
index_row_indices(&db, "idx_open"),
vec![2],
"deleting an included row should remove its index entry"
);
execute_sql(&mut db, "DELETE FROM orders WHERE id = 2");
let body = index_body(&db, "idx_open");
assert_eq!(body.len(), 1, "partial-index body must have exactly one entry after delete");
let surviving_row_idx = *body.values().next().unwrap().first().unwrap();
let surviving_row = &db.get_table("orders").unwrap().scan()[surviving_row_idx];
let id_val = &surviving_row.values[0];
let id_matches =
matches!(id_val, SqlValue::Integer(3) | SqlValue::Bigint(3) | SqlValue::Smallint(3));
assert!(
id_matches,
"partial-index body must reference the surviving predicate-truthy row (id=3); got {:?}",
id_val
);
}
#[test]
fn partial_unique_index_allows_duplicates_outside_predicate() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE orders (id INTEGER PRIMARY KEY, status INTEGER, sku INTEGER);
CREATE UNIQUE INDEX idx_open_sku ON orders(sku) WHERE status = 1;
INSERT INTO orders VALUES (1, 0, 42);
INSERT INTO orders VALUES (2, 0, 42);
"#,
);
let table = db.get_table("orders").expect("table missing");
assert_eq!(table.row_count(), 2);
assert!(
index_body(&db, "idx_open_sku").is_empty(),
"partial index must be empty when no row matches the predicate"
);
}
#[test]
fn partial_unique_index_rejects_duplicates_inside_predicate() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE orders (id INTEGER PRIMARY KEY, status INTEGER, sku INTEGER);
CREATE UNIQUE INDEX idx_open_sku ON orders(sku) WHERE status = 1;
INSERT INTO orders VALUES (1, 1, 42);
"#,
);
let stmt = Parser::parse_sql("INSERT INTO orders VALUES (2, 1, 42)").unwrap();
let result = match &stmt {
vibesql_ast::Statement::Insert(s) => InsertExecutor::execute(&mut db, s),
_ => unreachable!(),
};
assert!(
result.is_err(),
"partial UNIQUE index should reject duplicate keys within the predicate"
);
let table = db.get_table("orders").expect("table missing");
assert_eq!(table.row_count(), 1);
}
#[test]
fn partial_index_does_not_index_predicate_falsy_rows_on_insert() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE t (id INTEGER PRIMARY KEY, flag INTEGER);
CREATE INDEX idx_flag ON t(id) WHERE flag = 1;
INSERT INTO t VALUES (1, 0);
INSERT INTO t VALUES (2, 1);
INSERT INTO t VALUES (3, NULL);
"#,
);
let entries = index_row_indices(&db, "idx_flag");
assert_eq!(
entries,
vec![1],
"only the row whose predicate evaluated to truthy should be indexed; \
NULL (which is not truthy) and 0 (which is not equal to 1) must be excluded"
);
}
#[test]
fn batch_insert_into_partial_index_evaluates_predicate() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE t (id INTEGER PRIMARY KEY, flag INTEGER);
CREATE INDEX idx_flag ON t(id) WHERE flag = 1;
"#,
);
execute_sql(&mut db, "INSERT INTO t VALUES (1, 1), (2, 0), (3, 1), (4, 0), (5, 1)");
let entries = index_row_indices(&db, "idx_flag");
assert_eq!(entries, vec![0, 2, 4]);
}
fn assert_partial_index_consistent_after_compaction(
db: &Database,
table_name: &str,
index_name: &str,
expected_predicate_truthy_ids: &[i64],
id_col: usize,
) {
let table = db.get_table(table_name).expect("table missing");
let rows: Vec<_> = table.scan().to_vec();
let mut indexed_ids: Vec<i64> = index_row_indices(db, index_name)
.into_iter()
.map(|row_idx| {
let v = &rows[row_idx].values[id_col];
match v {
SqlValue::Integer(n) => *n,
SqlValue::Bigint(n) => *n,
SqlValue::Smallint(n) => *n as i64,
SqlValue::Double(n) => *n as i64,
SqlValue::Real(n) => *n as i64,
SqlValue::Float(n) => *n as i64,
other => panic!("unexpected id type in row: {:?}", other),
}
})
.collect();
indexed_ids.sort_unstable();
let mut expected: Vec<i64> = expected_predicate_truthy_ids.to_vec();
expected.sort_unstable();
assert_eq!(
indexed_ids, expected,
"partial-index body after compaction must reference exactly the rows whose predicate is truthy"
);
}
#[test]
fn partial_index_survives_table_compaction_after_bulk_delete() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE orders (id INTEGER PRIMARY KEY, status INTEGER);
"#,
);
let mut insert_sql = String::from("INSERT INTO orders VALUES ");
for id in 1..=20 {
let status = if id % 3 == 0 { 1 } else { 0 };
if id > 1 {
insert_sql.push_str(", ");
}
insert_sql.push_str(&format!("({}, {})", id, status));
}
execute_sql(&mut db, &insert_sql);
execute_sql(&mut db, "CREATE INDEX idx_open ON orders(id) WHERE status = 1");
let truthy_ids_before: Vec<i64> = vec![3, 6, 9, 12, 15, 18];
assert_partial_index_consistent_after_compaction(
&db,
"orders",
"idx_open",
&truthy_ids_before,
0,
);
execute_sql(&mut db, "DELETE FROM orders WHERE id NOT IN (3, 6, 9, 12, 15, 18, 20)");
let truthy_ids_after: Vec<i64> = vec![3, 6, 9, 12, 15, 18];
assert_partial_index_consistent_after_compaction(
&db,
"orders",
"idx_open",
&truthy_ids_after,
0,
);
let table = db.get_table("orders").expect("table missing");
assert_eq!(table.row_count(), 7, "compaction should have left 7 surviving rows");
}
#[test]
fn partial_unique_index_remains_enforceable_after_compaction() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE orders (id INTEGER PRIMARY KEY, status INTEGER, sku INTEGER);
"#,
);
let mut insert_sql = String::from("INSERT INTO orders VALUES ");
for id in 1..=20 {
let status = if id % 3 == 0 { 1 } else { 0 };
if id > 1 {
insert_sql.push_str(", ");
}
insert_sql.push_str(&format!("({}, {}, {})", id, status, 100 + id));
}
execute_sql(&mut db, &insert_sql);
execute_sql(&mut db, "CREATE UNIQUE INDEX idx_open_sku ON orders(sku) WHERE status = 1");
execute_sql(&mut db, "DELETE FROM orders WHERE id NOT IN (3, 6, 9, 12, 15, 18, 1, 2)");
let stmt = Parser::parse_sql("INSERT INTO orders VALUES (99, 1, 109)").unwrap();
let result = match &stmt {
vibesql_ast::Statement::Insert(s) => InsertExecutor::execute(&mut db, s),
_ => unreachable!(),
};
assert!(
result.is_err(),
"partial UNIQUE index must still reject duplicates after table compaction"
);
execute_sql(&mut db, "INSERT INTO orders VALUES (100, 1, 999)");
}
#[test]
fn partial_index_body_empty_after_compaction_removes_all_truthy_rows() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE orders (id INTEGER PRIMARY KEY, status INTEGER);
"#,
);
execute_sql(
&mut db,
"INSERT INTO orders VALUES \
(1, 0), (2, 0), (3, 0), (4, 1), (5, 0), \
(6, 0), (7, 1), (8, 0), (9, 0), (10, 0)",
);
execute_sql(&mut db, "CREATE INDEX idx_open ON orders(id) WHERE status = 1");
assert_eq!(index_row_indices(&db, "idx_open").len(), 2);
execute_sql(&mut db, "DELETE FROM orders WHERE id IN (1, 2, 4, 5, 7, 8)");
assert!(
index_body(&db, "idx_open").is_empty(),
"after compaction removes all truthy rows, partial-index body must be empty"
);
let table = db.get_table("orders").expect("table missing");
assert_eq!(table.row_count(), 4);
}
fn explain_query_plan(db: &Database, sql: &str) -> String {
let explain_sql = format!("EXPLAIN QUERY PLAN {}", sql);
let stmt = Parser::parse_sql(&explain_sql).expect("Failed to parse EXPLAIN QUERY PLAN");
if let vibesql_ast::Statement::Explain(explain_stmt) = stmt {
vibesql_executor::ExplainExecutor::execute(&explain_stmt, db)
.expect("EXPLAIN QUERY PLAN failed")
.to_text()
} else {
panic!("Expected EXPLAIN statement");
}
}
fn select_first_column_ints(db: &Database, sql: &str) -> Vec<i64> {
let stmt = Parser::parse_sql(sql).expect("Failed to parse SELECT");
if let vibesql_ast::Statement::Select(select_stmt) = stmt {
let executor = vibesql_executor::SelectExecutor::new(db);
let rows = executor.execute(&select_stmt).expect("SELECT failed");
rows.iter()
.map(|row| match &row.values[0] {
SqlValue::Integer(i) => *i,
other => panic!("expected integer, got {:?}", other),
})
.collect()
} else {
panic!("Expected SELECT statement");
}
}
#[test]
fn planner_selects_partial_expression_index_when_predicate_implied() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE t3 (a INTEGER PRIMARY KEY, b REAL);
INSERT INTO t3 VALUES (1, 2457939.5);
INSERT INTO t3 VALUES (2, 2457940.5);
INSERT INTO t3 VALUES (3, 2457950.5);
CREATE INDEX t3b1 ON t3(datetime(b)) WHERE typeof(b)='real'
"#,
);
let sql = "SELECT a FROM t3 WHERE typeof(b)='real' \
AND datetime(b) BETWEEN '2017-07-04' AND '2017-07-08'";
let plan = explain_query_plan(&db, sql);
assert!(
plan.contains("t3b1"),
"partial expression index must be selected when the predicate is implied (#5333), got:\n{}",
plan
);
let mut rows = select_first_column_ints(&db, sql);
rows.sort_unstable();
assert_eq!(rows, vec![1, 2], "query must return correct rows via the index probe");
}
#[test]
fn planner_skips_partial_expression_index_when_predicate_not_implied() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE t3 (a INTEGER PRIMARY KEY, b REAL);
INSERT INTO t3 VALUES (1, 2457939.5);
CREATE INDEX t3b1 ON t3(datetime(b)) WHERE typeof(b)='real'
"#,
);
let plan = explain_query_plan(
&db,
"SELECT a FROM t3 WHERE datetime(b) BETWEEN '2017-07-04' AND '2017-07-08'",
);
assert!(
!plan.contains("t3b1"),
"partial index must not be selected without an implying WHERE conjunct, got:\n{}",
plan
);
let plan_or = explain_query_plan(
&db,
"SELECT a FROM t3 WHERE typeof(b)='real' OR datetime(b) > '2017-07-04'",
);
assert!(
!plan_or.contains("t3b1"),
"top-level OR must not imply the index predicate, got:\n{}",
plan_or
);
}
#[test]
fn planner_selects_partial_non_expression_index_when_predicate_implied() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE orders (id INTEGER PRIMARY KEY, status INTEGER, sku INTEGER);
INSERT INTO orders VALUES (1, 0, 100);
INSERT INTO orders VALUES (2, 1, 200);
INSERT INTO orders VALUES (3, 1, 300);
INSERT INTO orders VALUES (4, 0, 300);
CREATE INDEX idx_open_sku ON orders(sku) WHERE status = 1
"#,
);
let implied_sql = "SELECT id FROM orders WHERE status = 1 AND sku = 300";
let plan = explain_query_plan(&db, implied_sql);
assert!(
plan.contains("USING INDEX idx_open_sku"),
"partial non-expression index must be selected when implied, got:\n{}",
plan
);
assert_eq!(select_first_column_ints(&db, implied_sql), vec![3]);
let not_implied_sql = "SELECT id FROM orders WHERE sku = 300";
let plan = explain_query_plan(&db, not_implied_sql);
assert!(
!plan.contains("idx_open_sku"),
"partial index must not be selected without the status conjunct, got:\n{}",
plan
);
let mut ids = select_first_column_ints(&db, not_implied_sql);
ids.sort_unstable();
assert_eq!(ids, vec![3, 4]);
}
#[test]
fn like_escape_predicate_is_not_implied_by_escapeless_like() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO t VALUES (1, 'x%y');
INSERT INTO t VALUES (2, 'x!zzy');
CREATE INDEX idx_name ON t(name) WHERE name LIKE 'x!%y' ESCAPE '!'
"#,
);
assert_eq!(index_row_indices(&db, "idx_name"), vec![0]);
let sql = "SELECT id FROM t WHERE name LIKE 'x!%y' AND name = 'x!zzy'";
let plan = explain_query_plan(&db, sql);
assert!(
!plan.contains("idx_name"),
"escape-less LIKE must not imply the LIKE ... ESCAPE index predicate, got:\n{}",
plan
);
assert_eq!(select_first_column_ints(&db, sql), vec![2]);
let implied_sql = "SELECT id FROM t WHERE name LIKE 'x!%y' ESCAPE '!' AND id = 1";
assert_eq!(select_first_column_ints(&db, implied_sql), vec![1]);
}
fn select_single_int(db: &Database, sql: &str) -> i64 {
let stmt = Parser::parse_sql(sql).expect("Failed to parse SELECT");
if let vibesql_ast::Statement::Select(select_stmt) = stmt {
let executor = vibesql_executor::SelectExecutor::new(db);
let rows = executor.execute(&select_stmt).expect("SELECT failed");
assert_eq!(rows.len(), 1, "expected exactly one result row");
match &rows[0].values[0] {
SqlValue::Integer(n) => *n,
SqlValue::Bigint(n) => *n,
SqlValue::Smallint(n) => *n as i64,
SqlValue::Double(n) => *n as i64,
SqlValue::Real(n) => *n as i64,
SqlValue::Float(n) => *n as i64,
other => panic!("expected numeric value, got {:?}", other),
}
} else {
panic!("Expected SELECT statement");
}
}
#[test]
fn pk_range_scan_fast_path_skips_unusable_partial_index() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE events (id INTEGER PRIMARY KEY, kind INTEGER);
INSERT INTO events VALUES (1, 1);
INSERT INTO events VALUES (2, 0);
INSERT INTO events VALUES (3, 1);
INSERT INTO events VALUES (4, 0);
INSERT INTO events VALUES (5, 1);
CREATE INDEX idx_kind1 ON events(id) WHERE kind = 1
"#,
);
assert_eq!(index_row_indices(&db, "idx_kind1"), vec![0, 2, 4]);
let mut ids = select_first_column_ints(&db, "SELECT id FROM events WHERE id BETWEEN 1 AND 5");
ids.sort_unstable();
assert_eq!(ids, vec![1, 2, 3, 4, 5], "range scan must not drop rows outside the partial index");
}
#[test]
fn streaming_aggregate_fast_path_skips_unusable_partial_index() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE metrics (id INTEGER PRIMARY KEY, val INTEGER, flag INTEGER);
INSERT INTO metrics VALUES (1, 10, 0);
INSERT INTO metrics VALUES (2, 20, 1);
INSERT INTO metrics VALUES (3, 30, 0);
INSERT INTO metrics VALUES (4, 40, 1);
CREATE INDEX idx_flag1 ON metrics(id) WHERE flag = 1
"#,
);
assert_eq!(index_row_indices(&db, "idx_flag1"), vec![1, 3]);
let sum = select_single_int(&db, "SELECT SUM(val) FROM metrics WHERE id BETWEEN 1 AND 4");
assert_eq!(sum, 100, "streaming aggregate must not drop rows outside the partial index");
let count = select_single_int(&db, "SELECT COUNT(val) FROM metrics WHERE id BETWEEN 1 AND 4");
assert_eq!(count, 4, "streaming COUNT must see all rows in the PK range");
}
#[test]
fn inl_semi_join_skips_partial_index_without_implying_filters() {
let mut db = Database::new();
execute_sql(
&mut db,
r#"
CREATE TABLE customers (id INTEGER PRIMARY KEY);
INSERT INTO customers VALUES (1);
INSERT INTO customers VALUES (2);
INSERT INTO customers VALUES (3);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, status INTEGER);
INSERT INTO orders VALUES (1, 1, 0);
INSERT INTO orders VALUES (2, 2, 1);
CREATE INDEX idx_open ON orders(customer_id) WHERE status = 1
"#,
);
assert_eq!(index_row_indices(&db, "idx_open"), vec![1]);
let mut ids = select_first_column_ints(
&db,
"SELECT id FROM customers WHERE id IN (SELECT customer_id FROM orders)",
);
ids.sort_unstable();
assert_eq!(ids, vec![1, 2], "semi join must not drop matches outside the partial index");
let ids_open = select_first_column_ints(
&db,
"SELECT id FROM customers WHERE id IN (SELECT customer_id FROM orders WHERE status = 1)",
);
assert_eq!(ids_open, vec![2]);
}