use vibesql_types::SqlValue;
use crate::database::{
indexes::{IndexData, IndexManager},
DatabaseConfig, SpillPolicy,
};
#[test]
fn test_range_scan_preserves_index_order() {
let mut data = std::collections::BTreeMap::new();
data.insert(vec![SqlValue::Double(50.0)], vec![1]);
data.insert(vec![SqlValue::Double(60.0)], vec![2]);
data.insert(vec![SqlValue::Double(70.0)], vec![0]);
let index_data = IndexData::InMemory { data };
let result = index_data.range_scan(
Some(&SqlValue::Integer(55)),
None,
false, false,
);
assert_eq!(
result,
vec![2, 0],
"range_scan should return rows in index order (by value), not row index order"
);
}
#[test]
fn test_range_scan_between_preserves_order() {
let mut data = std::collections::BTreeMap::new();
data.insert(vec![SqlValue::Double(40.0)], vec![5]);
data.insert(vec![SqlValue::Double(50.0)], vec![1]);
data.insert(vec![SqlValue::Double(60.0)], vec![2]);
data.insert(vec![SqlValue::Double(70.0)], vec![0]);
let index_data = IndexData::InMemory { data };
let result = index_data.range_scan(
Some(&SqlValue::Integer(45)),
Some(&SqlValue::Integer(65)),
true, true, );
assert_eq!(result, vec![1, 2], "BETWEEN should return rows in index order");
}
#[test]
fn test_range_scan_with_duplicate_values() {
let mut data = std::collections::BTreeMap::new();
data.insert(vec![SqlValue::Double(50.0)], vec![1]);
data.insert(vec![SqlValue::Double(60.0)], vec![3, 7, 2]); data.insert(vec![SqlValue::Double(70.0)], vec![0]);
let index_data = IndexData::InMemory { data };
let result = index_data.range_scan(
Some(&SqlValue::Integer(60)),
None,
true, false,
);
assert_eq!(
result,
vec![3, 7, 2, 0],
"Duplicate values should maintain insertion order within the same key"
);
}
#[test]
fn test_multi_lookup_with_duplicate_values() {
let mut data = std::collections::BTreeMap::new();
data.insert(vec![SqlValue::Double(50.0)], vec![1]);
data.insert(vec![SqlValue::Double(60.0)], vec![3, 7, 2]); data.insert(vec![SqlValue::Double(70.0)], vec![0]);
let index_data = IndexData::InMemory { data };
let result = index_data.multi_lookup(&[SqlValue::Integer(60), SqlValue::Integer(70)]);
assert_eq!(
result,
vec![3, 7, 2, 0],
"multi_lookup with duplicate values should maintain insertion order within the same key"
);
}
#[test]
fn test_disk_backed_index_creation_with_bulk_load() {
use vibesql_ast::OrderDirection;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::Row;
let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
let columns = vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)];
let table_schema = TableSchema::new("test_table".to_string(), columns);
let num_rows = 100_500;
let table_rows: Vec<Row> =
(0..num_rows).map(|i| Row::from_vec(vec![SqlValue::Integer(i as i64)])).collect();
let mut index_manager = IndexManager::new();
index_manager.set_database_path(temp_dir.path().to_path_buf());
let result = index_manager.create_index(
"idx_id".to_string(),
"test_table".to_string(),
"main",
&table_schema,
&table_rows,
false, vec![vibesql_ast::IndexColumn::Column {
column_name: "id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
None,
None,
);
assert!(result.is_ok(), "Large index creation should succeed");
assert!(index_manager.index_exists("idx_id"));
let index_data = index_manager.get_index_data("idx_id");
assert!(index_data.is_some(), "Index data should be retrievable");
}
#[test]
fn test_in_memory_index_for_small_tables() {
use vibesql_ast::OrderDirection;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::Row;
let columns = vec![ColumnSchema::new("value".to_string(), DataType::Integer, false)];
let table_schema = TableSchema::new("small_table".to_string(), columns);
let table_rows: Vec<Row> =
(0..100).map(|i| Row::from_vec(vec![SqlValue::Integer(i as i64)])).collect();
let mut index_manager = IndexManager::new();
let result = index_manager.create_index(
"idx_value".to_string(),
"small_table".to_string(),
"main",
&table_schema,
&table_rows,
false,
vec![vibesql_ast::IndexColumn::Column {
column_name: "value".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
None,
None,
);
assert!(result.is_ok());
assert!(index_manager.index_exists("idx_value"));
let index_data = index_manager.get_index_data("idx_value");
assert!(index_data.is_some());
match index_data.unwrap() {
IndexData::InMemory { .. } => {
}
IndexData::DiskBacked { .. } => {
panic!("Expected InMemory variant for small table, got DiskBacked");
}
IndexData::IVFFlat { .. } => {
panic!("Expected InMemory variant for small table, got IVFFlat");
}
IndexData::Hnsw { .. } => {
panic!("Expected InMemory variant for small table, got Hnsw");
}
}
}
#[test]
fn test_budget_enforcement_with_spill_policy() {
use vibesql_ast::OrderDirection;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::Row;
let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
let columns = vec![ColumnSchema::new("value".to_string(), DataType::Integer, false)];
let table_schema = TableSchema::new("test_table".to_string(), columns);
let table_rows: Vec<Row> =
(0..100).map(|i| Row::from_vec(vec![SqlValue::Integer(i as i64)])).collect();
let mut index_manager = IndexManager::new();
index_manager.set_database_path(temp_dir.path().to_path_buf());
let config = DatabaseConfig {
memory_budget: 1000, disk_budget: 100 * 1024 * 1024, spill_policy: SpillPolicy::SpillToDisk,
sql_mode: vibesql_types::SqlMode::default(),
columnar_cache_budget: 1024 * 1024, };
index_manager.set_config(config);
let result1 = index_manager.create_index(
"idx_1".to_string(),
"test_table".to_string(),
"main",
&table_schema,
&table_rows,
false,
vec![vibesql_ast::IndexColumn::Column {
column_name: "value".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
None,
None,
);
assert!(result1.is_ok());
let result2 = index_manager.create_index(
"idx_2".to_string(),
"test_table".to_string(),
"main",
&table_schema,
&table_rows,
false,
vec![vibesql_ast::IndexColumn::Column {
column_name: "value".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
None,
None,
);
assert!(result2.is_ok());
assert!(index_manager.index_exists("idx_1"));
assert!(index_manager.index_exists("idx_2"));
}
#[test]
fn test_lru_eviction_order() {
use std::thread;
use instant::Duration;
use vibesql_ast::OrderDirection;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::Row;
let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
let columns = vec![ColumnSchema::new("value".to_string(), DataType::Integer, false)];
let table_schema = TableSchema::new("test_table".to_string(), columns);
let table_rows: Vec<Row> =
(0..50).map(|i| Row::from_vec(vec![SqlValue::Integer(i as i64)])).collect();
let mut index_manager = IndexManager::new();
index_manager.set_database_path(temp_dir.path().to_path_buf());
let config = DatabaseConfig {
memory_budget: 2000, disk_budget: 100 * 1024 * 1024,
spill_policy: SpillPolicy::SpillToDisk,
sql_mode: vibesql_types::SqlMode::default(),
columnar_cache_budget: 1024 * 1024, };
index_manager.set_config(config);
index_manager
.create_index(
"idx_1".to_string(),
"test_table".to_string(),
"main",
&table_schema,
&table_rows,
false,
vec![vibesql_ast::IndexColumn::Column {
column_name: "value".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
None,
None,
)
.unwrap();
thread::sleep(Duration::from_millis(10));
index_manager
.create_index(
"idx_2".to_string(),
"test_table".to_string(),
"main",
&table_schema,
&table_rows,
false,
vec![vibesql_ast::IndexColumn::Column {
column_name: "value".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
None,
None,
)
.unwrap();
thread::sleep(Duration::from_millis(10));
let _ = index_manager.get_index_data("idx_1");
thread::sleep(Duration::from_millis(10));
index_manager
.create_index(
"idx_3".to_string(),
"test_table".to_string(),
"main",
&table_schema,
&table_rows,
false,
vec![vibesql_ast::IndexColumn::Column {
column_name: "value".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
None,
None,
)
.unwrap();
assert!(index_manager.index_exists("idx_1"));
assert!(index_manager.index_exists("idx_2"));
assert!(index_manager.index_exists("idx_3"));
let _backend_1 = index_manager.resource_tracker.get_backend("idx_1");
let backend_2 = index_manager.resource_tracker.get_backend("idx_2");
let _backend_3 = index_manager.resource_tracker.get_backend("idx_3");
assert!(backend_2.is_some());
}
#[test]
fn test_access_tracking() {
use vibesql_ast::OrderDirection;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::Row;
let columns = vec![ColumnSchema::new("value".to_string(), DataType::Integer, false)];
let table_schema = TableSchema::new("test_table".to_string(), columns);
let table_rows: Vec<Row> =
(0..10).map(|i| Row::from_vec(vec![SqlValue::Integer(i as i64)])).collect();
let mut index_manager = IndexManager::new();
index_manager
.create_index(
"idx_test".to_string(),
"test_table".to_string(),
"main",
&table_schema,
&table_rows,
false,
vec![vibesql_ast::IndexColumn::Column {
column_name: "value".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
None,
None,
)
.unwrap();
let stats = index_manager.resource_tracker.get_index_stats("IDX_TEST");
assert!(stats.is_some());
let initial_count = stats.unwrap().get_access_count();
let _ = index_manager.get_index_data("IDX_TEST");
let _ = index_manager.get_index_data("IDX_TEST");
let _ = index_manager.get_index_data("IDX_TEST");
let stats = index_manager.resource_tracker.get_index_stats("IDX_TEST");
assert!(stats.is_some());
let final_count = stats.unwrap().get_access_count();
assert!(
final_count > initial_count,
"Access count should increase after index accesses (initial: {}, final: {})",
initial_count,
final_count
);
}
#[test]
fn test_resource_cleanup_on_drop() {
use vibesql_ast::OrderDirection;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::Row;
let columns = vec![ColumnSchema::new("value".to_string(), DataType::Integer, false)];
let table_schema = TableSchema::new("test_table".to_string(), columns);
let table_rows: Vec<Row> =
(0..100).map(|i| Row::from_vec(vec![SqlValue::Integer(i as i64)])).collect();
let mut index_manager = IndexManager::new();
index_manager
.create_index(
"idx_test".to_string(),
"test_table".to_string(),
"main",
&table_schema,
&table_rows,
false,
vec![vibesql_ast::IndexColumn::Column {
column_name: "value".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
None,
None,
)
.unwrap();
let memory_before = index_manager.resource_tracker.memory_used();
assert!(memory_before > 0);
index_manager.drop_index("idx_test").unwrap();
let memory_after = index_manager.resource_tracker.memory_used();
assert_eq!(memory_after, 0, "Memory should be freed after dropping index");
}
#[test]
fn test_database_config_presets() {
let browser_config = DatabaseConfig::browser_default();
assert_eq!(browser_config.memory_budget, 512 * 1024 * 1024); assert_eq!(browser_config.disk_budget, 2 * 1024 * 1024 * 1024); assert_eq!(browser_config.spill_policy, SpillPolicy::SpillToDisk);
let server_config = DatabaseConfig::server_default();
assert_eq!(server_config.memory_budget, 16 * 1024 * 1024 * 1024); assert_eq!(server_config.disk_budget, 1024 * 1024 * 1024 * 1024); assert_eq!(server_config.spill_policy, SpillPolicy::BestEffort);
let test_config = DatabaseConfig::test_default();
assert_eq!(test_config.memory_budget, 10 * 1024 * 1024); assert_eq!(test_config.disk_budget, 100 * 1024 * 1024); assert_eq!(test_config.spill_policy, SpillPolicy::SpillToDisk);
}
#[test]
fn test_index_scan_after_database_reset() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::{Database, Row};
fn run_test_cycle(db: &mut Database, cycle_num: usize) -> Result<(), String> {
eprintln!("\n=== Test Cycle {} ===", cycle_num);
let columns = vec![
ColumnSchema::new("pk".to_string(), DataType::Integer, false),
ColumnSchema::new("col0".to_string(), DataType::Integer, false),
];
let mut table_schema = TableSchema::new("tab1".to_string(), columns);
table_schema.primary_key = Some(vec!["pk".to_string()]);
db.create_table(table_schema.clone()).unwrap();
eprintln!(" Created table 'tab1'");
let rows = vec![
Row::from_vec(vec![SqlValue::Integer(1), SqlValue::Integer(100)]),
Row::from_vec(vec![SqlValue::Integer(2), SqlValue::Integer(200)]),
Row::from_vec(vec![SqlValue::Integer(3), SqlValue::Integer(300)]),
Row::from_vec(vec![SqlValue::Integer(4), SqlValue::Integer(400)]),
Row::from_vec(vec![SqlValue::Integer(5), SqlValue::Integer(500)]),
];
let table = db.get_table_mut("tab1").unwrap();
for row in &rows {
table.insert(row.clone()).unwrap();
}
eprintln!(" Inserted {} rows", rows.len());
db.create_index(
"idx_col0".to_string(),
"tab1".to_string(),
false,
vec![IndexColumn::Column {
column_name: "col0".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
)
.unwrap();
eprintln!(" Created index 'idx_col0'");
assert!(db.index_exists("idx_col0"));
let index_data = db.get_index_data("idx_col0").expect("Index should exist");
let all_indices: Vec<usize> = match &index_data {
crate::database::indexes::IndexData::InMemory { data, .. } => {
data.values().flatten().copied().collect()
}
crate::database::indexes::IndexData::DiskBacked { .. } => {
panic!("Expected in-memory index for small table");
}
crate::database::indexes::IndexData::IVFFlat { .. } => {
panic!("Expected in-memory index for small table, got IVFFlat");
}
crate::database::indexes::IndexData::Hnsw { .. } => {
panic!("Expected in-memory index for small table, got Hnsw");
}
};
eprintln!(" Index contains {} row references", all_indices.len());
assert_eq!(all_indices.len(), 5, "Index should contain 5 row references");
let table = db.get_table("tab1").expect("Table should exist");
let matching_row_indices = index_data.range_scan(
Some(&SqlValue::Integer(250)), None,
false, false,
);
eprintln!(" Index range scan returned {} row indices", matching_row_indices.len());
if matching_row_indices.len() != 3 {
return Err(format!(
"Cycle {}: Index scan should find 3 rows with col0 > 250, but found {}",
cycle_num,
matching_row_indices.len()
));
}
let all_rows = table.scan();
eprintln!(" Table has {} total rows", all_rows.len());
let fetched_rows: Vec<Row> =
matching_row_indices.into_iter().filter_map(|idx| all_rows.get(idx).cloned()).collect();
eprintln!(" Fetched {} rows from table using index", fetched_rows.len());
if fetched_rows.len() != 3 {
return Err(format!(
"Cycle {}: Should fetch 3 rows from table using index scan, but got {} rows. \
Table has {} total rows. This reproduces issue #1618!",
cycle_num,
fetched_rows.len(),
all_rows.len()
));
}
assert_eq!(fetched_rows[0].values[1], SqlValue::Integer(300));
assert_eq!(fetched_rows[1].values[1], SqlValue::Integer(400));
assert_eq!(fetched_rows[2].values[1], SqlValue::Integer(500));
eprintln!(" ✓ Cycle {} PASSED", cycle_num);
Ok(())
}
let mut db = Database::new();
eprintln!("\n🔄 Running first test file (fresh database)...");
run_test_cycle(&mut db, 1).expect("Cycle 1 should pass (fresh database)");
eprintln!("\n🔄 Resetting database (simulating pooling)...");
db.reset();
eprintln!("🔄 Running second test file (pooled database after reset)...");
match run_test_cycle(&mut db, 2) {
Ok(()) => {
eprintln!("\n⚠️ WARNING: Cycle 2 passed! The bug may have been fixed.");
eprintln!(" If this test now passes, issue #1618 is resolved.");
}
Err(e) => {
panic!("\n❌ REPRODUCED ISSUE #1618:\n{}\n\nThis demonstrates the database pooling bug where index scans return 0 rows after Database::reset()", e);
}
}
}
#[test]
fn test_thread_local_pool_pattern() {
use std::cell::RefCell;
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
thread_local! {
static TEST_DB_POOL: RefCell<Option<Database>> = const { RefCell::new(None) };
}
fn get_pooled_database() -> Database {
TEST_DB_POOL.with(|pool| {
let mut pool_ref = pool.borrow_mut();
match pool_ref.take() {
Some(mut db) => {
eprintln!(" [POOL] Reusing pooled database after reset");
db.reset();
db
}
None => {
eprintln!(" [POOL] Creating fresh database");
Database::new()
}
}
})
}
fn return_to_pool(db: Database) {
TEST_DB_POOL.with(|pool| {
let mut pool_ref = pool.borrow_mut();
if pool_ref.is_none() {
eprintln!(" [POOL] Returning database to pool");
*pool_ref = Some(db);
}
});
}
fn run_cycle(cycle_num: usize) {
eprintln!("\n=== Cycle {} ===", cycle_num);
let mut db = get_pooled_database();
let columns = vec![
ColumnSchema::new("pk".to_string(), DataType::Integer, false),
ColumnSchema::new("col0".to_string(), DataType::Integer, false),
];
let mut table_schema = TableSchema::new("tab1".to_string(), columns);
table_schema.primary_key = Some(vec!["pk".to_string()]);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("tab1").unwrap();
for i in 1..=5 {
table
.insert(Row::from_vec(vec![SqlValue::Integer(i), SqlValue::Integer(i * 100)]))
.unwrap();
}
eprintln!(" Inserted 5 rows");
db.create_index(
"idx_col0".to_string(),
"tab1".to_string(),
false,
vec![IndexColumn::Column {
column_name: "col0".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
)
.unwrap();
eprintln!(" Created index");
let index_data = db.get_index_data("idx_col0").unwrap();
let matching_indices =
index_data.range_scan(Some(&SqlValue::Integer(250)), None, false, false);
eprintln!(" Index scan returned {} indices", matching_indices.len());
let table = db.get_table("tab1").unwrap();
let all_rows = table.scan();
let fetched_rows: Vec<Row> =
matching_indices.into_iter().filter_map(|idx| all_rows.get(idx).cloned()).collect();
eprintln!(" Fetched {} rows from table", fetched_rows.len());
assert_eq!(
fetched_rows.len(),
3,
"Cycle {}: Expected 3 rows, got {}",
cycle_num,
fetched_rows.len()
);
eprintln!(" ✓ Cycle {} PASSED", cycle_num);
return_to_pool(db);
}
run_cycle(1);
run_cycle(2);
run_cycle(3);
}
#[test]
fn test_lookup_by_index_single_column() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
let mut db = Database::new();
let columns = vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("name".to_string(), DataType::Varchar { max_length: Some(50) }, false),
];
let mut table_schema = TableSchema::new("users".to_string(), columns);
table_schema.primary_key = Some(vec!["id".to_string()]);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("users").unwrap();
table
.insert(Row::from_vec(vec![SqlValue::Integer(1), SqlValue::Varchar("Alice".into())]))
.unwrap();
table
.insert(Row::from_vec(vec![SqlValue::Integer(2), SqlValue::Varchar("Bob".into())]))
.unwrap();
table
.insert(Row::from_vec(vec![SqlValue::Integer(3), SqlValue::Varchar("Charlie".into())]))
.unwrap();
db.create_index(
"idx_users_id".to_string(),
"users".to_string(),
true, vec![IndexColumn::Column {
column_name: "id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
)
.unwrap();
let result = db.lookup_by_index("idx_users_id", &[SqlValue::Integer(2)]).unwrap();
assert!(result.is_some());
let rows = result.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].values[1], SqlValue::Varchar("Bob".into()));
let result = db.lookup_by_index("idx_users_id", &[SqlValue::Integer(999)]).unwrap();
assert!(result.is_none());
}
#[test]
fn test_lookup_one_by_index() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
let mut db = Database::new();
let columns = vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("value".to_string(), DataType::Integer, false),
];
let mut table_schema = TableSchema::new("items".to_string(), columns);
table_schema.primary_key = Some(vec!["id".to_string()]);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("items").unwrap();
table.insert(Row::from_vec(vec![SqlValue::Integer(1), SqlValue::Integer(100)])).unwrap();
table.insert(Row::from_vec(vec![SqlValue::Integer(2), SqlValue::Integer(200)])).unwrap();
db.create_index(
"idx_items_pk".to_string(),
"items".to_string(),
true,
vec![IndexColumn::Column {
column_name: "id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
)
.unwrap();
let row = db.lookup_one_by_index("idx_items_pk", &[SqlValue::Integer(1)]).unwrap();
assert!(row.is_some());
assert_eq!(row.unwrap().values[1], SqlValue::Integer(100));
let row = db.lookup_one_by_index("idx_items_pk", &[SqlValue::Integer(999)]).unwrap();
assert!(row.is_none());
}
#[test]
fn test_lookup_by_index_composite_key() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
let mut db = Database::new();
let columns = vec![
ColumnSchema::new("w_id".to_string(), DataType::Integer, false),
ColumnSchema::new("d_id".to_string(), DataType::Integer, false),
ColumnSchema::new("o_id".to_string(), DataType::Integer, false),
ColumnSchema::new("amount".to_string(), DataType::DoublePrecision, false),
];
let table_schema = TableSchema::new("orders".to_string(), columns);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("orders").unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(1),
SqlValue::Integer(1),
SqlValue::Double(100.0),
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(1),
SqlValue::Integer(2),
SqlValue::Double(200.0),
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(2),
SqlValue::Integer(1),
SqlValue::Double(300.0),
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(2),
SqlValue::Integer(1),
SqlValue::Integer(1),
SqlValue::Double(400.0),
]))
.unwrap();
db.create_index(
"idx_orders_pk".to_string(),
"orders".to_string(),
true,
vec![
IndexColumn::Column {
column_name: "w_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
IndexColumn::Column {
column_name: "d_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
IndexColumn::Column {
column_name: "o_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
],
)
.unwrap();
let result = db
.lookup_by_index(
"idx_orders_pk",
&[SqlValue::Integer(1), SqlValue::Integer(2), SqlValue::Integer(1)],
)
.unwrap();
assert!(result.is_some());
let rows = result.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].values[3], SqlValue::Double(300.0));
let result = db
.lookup_by_index(
"idx_orders_pk",
&[SqlValue::Integer(9), SqlValue::Integer(9), SqlValue::Integer(9)],
)
.unwrap();
assert!(result.is_none());
}
#[test]
fn test_lookup_by_index_batch() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
let mut db = Database::new();
let columns = vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("name".to_string(), DataType::Varchar { max_length: Some(50) }, false),
];
let table_schema = TableSchema::new("products".to_string(), columns);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("products").unwrap();
for i in 1..=10 {
table
.insert(Row::from_vec(vec![
SqlValue::Integer(i),
SqlValue::Varchar(arcstr::ArcStr::from(format!("Product{}", i))),
]))
.unwrap();
}
db.create_index(
"idx_products_pk".to_string(),
"products".to_string(),
true,
vec![IndexColumn::Column {
column_name: "id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
)
.unwrap();
let keys = vec![
vec![SqlValue::Integer(2)],
vec![SqlValue::Integer(5)],
vec![SqlValue::Integer(999)], vec![SqlValue::Integer(8)],
];
let results = db.lookup_by_index_batch("idx_products_pk", &keys).unwrap();
assert_eq!(results.len(), 4);
assert!(results[0].is_some()); assert!(results[1].is_some()); assert!(results[2].is_none()); assert!(results[3].is_some());
assert_eq!(results[0].as_ref().unwrap()[0].values[1], SqlValue::Varchar("Product2".into()));
assert_eq!(results[1].as_ref().unwrap()[0].values[1], SqlValue::Varchar("Product5".into()));
assert_eq!(results[3].as_ref().unwrap()[0].values[1], SqlValue::Varchar("Product8".into()));
}
#[test]
fn test_lookup_one_by_index_batch() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
let mut db = Database::new();
let columns = vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("price".to_string(), DataType::DoublePrecision, false),
];
let table_schema = TableSchema::new("items".to_string(), columns);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("items").unwrap();
for i in 1..=5 {
table
.insert(Row::from_vec(vec![SqlValue::Integer(i), SqlValue::Double(i as f64 * 10.0)]))
.unwrap();
}
db.create_index(
"idx_items_id".to_string(),
"items".to_string(),
true,
vec![IndexColumn::Column {
column_name: "id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
}],
)
.unwrap();
let keys = vec![
vec![SqlValue::Integer(1)],
vec![SqlValue::Integer(3)],
vec![SqlValue::Integer(99)], ];
let results = db.lookup_one_by_index_batch("idx_items_id", &keys).unwrap();
assert_eq!(results.len(), 3);
assert!(results[0].is_some());
assert!(results[1].is_some());
assert!(results[2].is_none());
assert_eq!(results[0].unwrap().values[1], SqlValue::Double(10.0));
assert_eq!(results[1].unwrap().values[1], SqlValue::Double(30.0));
}
#[test]
fn test_lookup_by_index_error_cases() {
use crate::Database;
let db = Database::new();
let result = db.lookup_by_index("nonexistent_index", &[SqlValue::Integer(1)]);
assert!(result.is_err());
let result = db.lookup_one_by_index("nonexistent_index", &[SqlValue::Integer(1)]);
assert!(result.is_err());
let result = db.lookup_by_index_batch("nonexistent_index", &[vec![SqlValue::Integer(1)]]);
assert!(result.is_err());
}
#[test]
fn test_lookup_by_index_prefix_basic() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
let mut db = Database::new();
let columns = vec![
ColumnSchema::new("w_id".to_string(), DataType::Integer, false),
ColumnSchema::new("d_id".to_string(), DataType::Integer, false),
ColumnSchema::new("o_id".to_string(), DataType::Integer, false),
ColumnSchema::new("carrier_id".to_string(), DataType::Integer, true),
];
let table_schema = TableSchema::new("new_order".to_string(), columns);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("new_order").unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(1),
SqlValue::Integer(101),
SqlValue::Null,
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(1),
SqlValue::Integer(102),
SqlValue::Null,
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(2),
SqlValue::Integer(201),
SqlValue::Null,
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(2),
SqlValue::Integer(202),
SqlValue::Null,
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(2),
SqlValue::Integer(203),
SqlValue::Null,
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(3),
SqlValue::Integer(301),
SqlValue::Null,
]))
.unwrap();
db.create_index(
"idx_no_pk".to_string(),
"new_order".to_string(),
true,
vec![
IndexColumn::Column {
column_name: "w_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
IndexColumn::Column {
column_name: "d_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
IndexColumn::Column {
column_name: "o_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
],
)
.unwrap();
let rows = db
.lookup_by_index_prefix("idx_no_pk", &[SqlValue::Integer(1), SqlValue::Integer(2)])
.unwrap();
assert_eq!(rows.len(), 3, "Should find 3 orders for district 2");
let order_ids: Vec<i64> = rows
.iter()
.map(|r| match &r.values[2] {
SqlValue::Integer(v) => *v,
_ => 0,
})
.collect();
assert!(order_ids.contains(&201));
assert!(order_ids.contains(&202));
assert!(order_ids.contains(&203));
}
#[test]
fn test_lookup_by_index_prefix_single_column() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
let mut db = Database::new();
let columns = vec![
ColumnSchema::new("warehouse".to_string(), DataType::Integer, false),
ColumnSchema::new("district".to_string(), DataType::Integer, false),
ColumnSchema::new("value".to_string(), DataType::Integer, false),
];
let table_schema = TableSchema::new("test_data".to_string(), columns);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("test_data").unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(1),
SqlValue::Integer(100),
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(2),
SqlValue::Integer(200),
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(3),
SqlValue::Integer(300),
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(2),
SqlValue::Integer(1),
SqlValue::Integer(400),
]))
.unwrap();
table
.insert(Row::from_vec(vec![
SqlValue::Integer(2),
SqlValue::Integer(2),
SqlValue::Integer(500),
]))
.unwrap();
db.create_index(
"idx_wd".to_string(),
"test_data".to_string(),
false,
vec![
IndexColumn::Column {
column_name: "warehouse".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
IndexColumn::Column {
column_name: "district".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
],
)
.unwrap();
let rows = db.lookup_by_index_prefix("idx_wd", &[SqlValue::Integer(1)]).unwrap();
assert_eq!(rows.len(), 3, "Should find 3 rows for warehouse 1");
let rows = db.lookup_by_index_prefix("idx_wd", &[SqlValue::Integer(2)]).unwrap();
assert_eq!(rows.len(), 2, "Should find 2 rows for warehouse 2");
}
#[test]
fn test_lookup_by_index_prefix_no_match() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
let mut db = Database::new();
let columns = vec![
ColumnSchema::new("a".to_string(), DataType::Integer, false),
ColumnSchema::new("b".to_string(), DataType::Integer, false),
];
let table_schema = TableSchema::new("test_table".to_string(), columns);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("test_table").unwrap();
table.insert(Row::from_vec(vec![SqlValue::Integer(1), SqlValue::Integer(10)])).unwrap();
table.insert(Row::from_vec(vec![SqlValue::Integer(2), SqlValue::Integer(20)])).unwrap();
db.create_index(
"idx_ab".to_string(),
"test_table".to_string(),
false,
vec![
IndexColumn::Column {
column_name: "a".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
IndexColumn::Column {
column_name: "b".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
],
)
.unwrap();
let rows = db.lookup_by_index_prefix("idx_ab", &[SqlValue::Integer(999)]).unwrap();
assert!(rows.is_empty(), "Should find no rows for non-existent prefix");
}
#[test]
fn test_lookup_by_index_prefix_batch_basic() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
let mut db = Database::new();
let columns = vec![
ColumnSchema::new("w_id".to_string(), DataType::Integer, false),
ColumnSchema::new("d_id".to_string(), DataType::Integer, false),
ColumnSchema::new("o_id".to_string(), DataType::Integer, false),
];
let table_schema = TableSchema::new("new_order".to_string(), columns);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("new_order").unwrap();
for d_id in 1..=10 {
for o_id in 1..=d_id {
table
.insert(Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Integer(d_id as i64),
SqlValue::Integer(o_id as i64 * 100),
]))
.unwrap();
}
}
db.create_index(
"idx_no_pk".to_string(),
"new_order".to_string(),
true,
vec![
IndexColumn::Column {
column_name: "w_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
IndexColumn::Column {
column_name: "d_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
IndexColumn::Column {
column_name: "o_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
],
)
.unwrap();
let prefixes = vec![
vec![SqlValue::Integer(1), SqlValue::Integer(1)], vec![SqlValue::Integer(1), SqlValue::Integer(5)], vec![SqlValue::Integer(1), SqlValue::Integer(10)], ];
let results = db.lookup_by_index_prefix_batch("idx_no_pk", &prefixes).unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0].len(), 1, "District 1 should have 1 order");
assert_eq!(results[1].len(), 5, "District 5 should have 5 orders");
assert_eq!(results[2].len(), 10, "District 10 should have 10 orders");
}
#[test]
fn test_lookup_by_index_prefix_batch_tpcc_delivery() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
let mut db = Database::new();
let columns = vec![
ColumnSchema::new("no_w_id".to_string(), DataType::Integer, false),
ColumnSchema::new("no_d_id".to_string(), DataType::Integer, false),
ColumnSchema::new("no_o_id".to_string(), DataType::Integer, false),
];
let table_schema = TableSchema::new("new_order".to_string(), columns);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("new_order").unwrap();
let w_id = 1;
for d_id in 1..=10 {
for o_id in 2100..(2100 + d_id) {
table
.insert(Row::from_vec(vec![
SqlValue::Integer(w_id),
SqlValue::Integer(d_id as i64),
SqlValue::Integer(o_id as i64),
]))
.unwrap();
}
}
db.create_index(
"idx_new_order_pk".to_string(),
"new_order".to_string(),
true,
vec![
IndexColumn::Column {
column_name: "no_w_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
IndexColumn::Column {
column_name: "no_d_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
IndexColumn::Column {
column_name: "no_o_id".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
],
)
.unwrap();
let prefixes: Vec<Vec<SqlValue>> =
(1..=10).map(|d| vec![SqlValue::Integer(w_id), SqlValue::Integer(d)]).collect();
let results = db.lookup_by_index_prefix_batch("idx_new_order_pk", &prefixes).unwrap();
assert_eq!(results.len(), 10);
for (d_idx, rows) in results.iter().enumerate() {
let d_id = d_idx + 1;
let expected_orders = d_id; assert_eq!(
rows.len(),
expected_orders,
"District {} should have {} new orders",
d_id,
expected_orders
);
}
}
#[test]
fn test_lookup_by_index_prefix_error_cases() {
use crate::Database;
let db = Database::new();
let result = db.lookup_by_index_prefix("nonexistent_index", &[SqlValue::Integer(1)]);
assert!(result.is_err());
let result =
db.lookup_by_index_prefix_batch("nonexistent_index", &[vec![SqlValue::Integer(1)]]);
assert!(result.is_err());
}
#[test]
fn test_lookup_by_index_prefix_empty_prefix() {
use vibesql_ast::{IndexColumn, OrderDirection};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::{Database, Row};
let mut db = Database::new();
let columns = vec![
ColumnSchema::new("a".to_string(), DataType::Integer, false),
ColumnSchema::new("b".to_string(), DataType::Integer, false),
];
let table_schema = TableSchema::new("test_table".to_string(), columns);
db.create_table(table_schema).unwrap();
let table = db.get_table_mut("test_table").unwrap();
table.insert(Row::from_vec(vec![SqlValue::Integer(1), SqlValue::Integer(10)])).unwrap();
db.create_index(
"idx_ab".to_string(),
"test_table".to_string(),
false,
vec![
IndexColumn::Column {
column_name: "a".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
IndexColumn::Column {
column_name: "b".to_string(),
direction: OrderDirection::Asc,
prefix_length: None,
},
],
)
.unwrap();
let rows = db.lookup_by_index_prefix("idx_ab", &[]).unwrap();
assert_eq!(rows.len(), 1, "Empty prefix should return all rows");
}
#[test]
fn test_range_scan_limit_basic() {
let mut data = std::collections::BTreeMap::new();
for i in 1..=10 {
data.insert(vec![SqlValue::Double((i * 10) as f64)], vec![i as usize]);
}
let index_data = IndexData::InMemory { data };
let result = index_data.range_scan_limit(None, None, false, false, Some(3));
assert_eq!(result.len(), 3, "Should return exactly 3 rows");
let result = index_data.range_scan_limit(
Some(&SqlValue::Integer(25)),
Some(&SqlValue::Integer(85)),
true,
true,
Some(2),
);
assert_eq!(result.len(), 2, "Should return exactly 2 rows with limit");
assert_eq!(result, vec![3, 4]);
}
#[test]
fn test_range_scan_limit_no_limit() {
let mut data = std::collections::BTreeMap::new();
for i in 1..=5 {
data.insert(vec![SqlValue::Double((i * 10) as f64)], vec![i as usize]);
}
let index_data = IndexData::InMemory { data };
let result = index_data.range_scan_limit(None, None, false, false, None);
assert_eq!(result.len(), 5, "Should return all rows when limit is None");
}
#[test]
fn test_range_scan_limit_zero() {
let mut data = std::collections::BTreeMap::new();
for i in 1..=5 {
data.insert(vec![SqlValue::Double((i * 10) as f64)], vec![i as usize]);
}
let index_data = IndexData::InMemory { data };
let result = index_data.range_scan_limit(None, None, false, false, Some(0));
assert!(result.is_empty(), "Should return empty when limit is 0");
}
#[test]
fn test_range_scan_limit_larger_than_result() {
let mut data = std::collections::BTreeMap::new();
for i in 1..=3 {
data.insert(vec![SqlValue::Double((i * 10) as f64)], vec![i as usize]);
}
let index_data = IndexData::InMemory { data };
let result = index_data.range_scan_limit(None, None, false, false, Some(100));
assert_eq!(result.len(), 3, "Should return all 3 rows when limit > result size");
}