Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 54 additions & 10 deletions crates/persistence/src/backends/sqlite/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use rusqlite::Connection;
use crate::error::StorageResult;

/// Current schema version.
pub const SCHEMA_VERSION: i32 = 19;
pub const SCHEMA_VERSION: i32 = 20;

/// Initialize the database schema.
pub fn initialize_schema(conn: &Connection) -> StorageResult<()> {
Expand Down Expand Up @@ -206,21 +206,21 @@ fn create_indexes(conn: &Connection) -> StorageResult<()> {
"CREATE INDEX IF NOT EXISTS idx_history_resource ON resource_history(tenant_id, resource_type, id)",
"CREATE INDEX IF NOT EXISTS idx_history_updated ON resource_history(tenant_id, last_updated)",
// Search index indexes
"CREATE INDEX IF NOT EXISTS idx_search_string ON search_index(tenant_id, resource_type, param_name, value_string)",
"CREATE INDEX IF NOT EXISTS idx_search_token ON search_index(tenant_id, resource_type, param_name, value_token_system, value_token_code)",
"CREATE INDEX IF NOT EXISTS idx_search_date ON search_index(tenant_id, resource_type, param_name, value_date)",
"CREATE INDEX IF NOT EXISTS idx_search_number ON search_index(tenant_id, resource_type, param_name, value_number)",
"CREATE INDEX IF NOT EXISTS idx_search_quantity ON search_index(tenant_id, resource_type, param_name, value_quantity_value, value_quantity_unit)",
"CREATE INDEX IF NOT EXISTS idx_search_reference ON search_index(tenant_id, resource_type, param_name, value_reference)",
"CREATE INDEX IF NOT EXISTS idx_search_uri ON search_index(tenant_id, resource_type, param_name, value_uri)",
"CREATE INDEX IF NOT EXISTS idx_search_string ON search_index(tenant_id, resource_type, param_name, value_string) WHERE value_string IS NOT NULL",
"CREATE INDEX IF NOT EXISTS idx_search_token ON search_index(tenant_id, resource_type, param_name, value_token_system, value_token_code) WHERE value_token_system IS NOT NULL OR value_token_code IS NOT NULL",
"CREATE INDEX IF NOT EXISTS idx_search_date ON search_index(tenant_id, resource_type, param_name, value_date) WHERE value_date IS NOT NULL",
"CREATE INDEX IF NOT EXISTS idx_search_number ON search_index(tenant_id, resource_type, param_name, value_number) WHERE value_number IS NOT NULL",
"CREATE INDEX IF NOT EXISTS idx_search_quantity ON search_index(tenant_id, resource_type, param_name, value_quantity_value, value_quantity_unit) WHERE value_quantity_value IS NOT NULL",
"CREATE INDEX IF NOT EXISTS idx_search_reference ON search_index(tenant_id, resource_type, param_name, value_reference) WHERE value_reference IS NOT NULL",
"CREATE INDEX IF NOT EXISTS idx_search_uri ON search_index(tenant_id, resource_type, param_name, value_uri) WHERE value_uri IS NOT NULL",
// Index for composite parameter matching
"CREATE INDEX IF NOT EXISTS idx_search_composite ON search_index(tenant_id, resource_type, resource_id, param_name, composite_group)",
// Index for resource-based lookups
"CREATE INDEX IF NOT EXISTS idx_search_resource ON search_index(tenant_id, resource_type, resource_id)",
// Index for :text modifier searches (token display text)
"CREATE INDEX IF NOT EXISTS idx_search_token_display ON search_index(tenant_id, resource_type, param_name, value_token_display)",
"CREATE INDEX IF NOT EXISTS idx_search_token_display ON search_index(tenant_id, resource_type, param_name, value_token_display) WHERE value_token_display IS NOT NULL",
// Index for :of-type modifier searches (identifier type)
"CREATE INDEX IF NOT EXISTS idx_search_identifier_type ON search_index(tenant_id, resource_type, param_name, value_identifier_type_system, value_identifier_type_code)",
"CREATE INDEX IF NOT EXISTS idx_search_identifier_type ON search_index(tenant_id, resource_type, param_name, value_identifier_type_system, value_identifier_type_code) WHERE value_identifier_type_system IS NOT NULL OR value_identifier_type_code IS NOT NULL",
];

for index_sql in &indexes {
Expand Down Expand Up @@ -301,6 +301,7 @@ fn migrate_schema(conn: &Connection, from_version: i32) -> StorageResult<()> {
16 => migrate_v16_to_v17(conn)?,
17 => migrate_v17_to_v18(conn)?,
18 => migrate_v18_to_v19(conn)?,
19 => migrate_v19_to_v20(conn)?,
_ => {
return Err(crate::error::StorageError::Backend(
crate::error::BackendError::Internal {
Expand Down Expand Up @@ -1442,6 +1443,49 @@ fn migrate_v18_to_v19(conn: &Connection) -> StorageResult<()> {
Ok(())
}

/// Migrate from schema version 19 to version 20.
///
/// Rebuilds the nine per-family `search_index` indexes as partial indexes.
/// Every index row populates exactly one value-column family for its
/// parameter type, but each INSERT maintained all fifteen secondary indexes —
/// NULL columns included, so a token row still paid the date, number,
/// quantity, string, and uri B-trees. Instrumentation on the bulk-import
/// benchmark put those inserts at 53% of total import time; with the partial
/// predicates each row maintains only its own family's structures, measured
/// at 1.67x end-to-end on the same benchmark with identical row counts.
///
/// Search plans are unaffected: every family's query predicates compare its
/// value column (`value_date >= ?`, `value_token_code = ?`), which implies
/// the index's `IS NOT NULL` (or OR-of-columns) predicate. `:missing` never
/// scans value columns for NULL — it resolves from entry presence.
fn migrate_v19_to_v20(conn: &Connection) -> StorageResult<()> {
let statements = [
"DROP INDEX IF EXISTS idx_search_string",
"CREATE INDEX idx_search_string ON search_index(tenant_id, resource_type, param_name, value_string) WHERE value_string IS NOT NULL",
"DROP INDEX IF EXISTS idx_search_token",
"CREATE INDEX idx_search_token ON search_index(tenant_id, resource_type, param_name, value_token_system, value_token_code) WHERE value_token_system IS NOT NULL OR value_token_code IS NOT NULL",
"DROP INDEX IF EXISTS idx_search_date",
"CREATE INDEX idx_search_date ON search_index(tenant_id, resource_type, param_name, value_date) WHERE value_date IS NOT NULL",
"DROP INDEX IF EXISTS idx_search_number",
"CREATE INDEX idx_search_number ON search_index(tenant_id, resource_type, param_name, value_number) WHERE value_number IS NOT NULL",
"DROP INDEX IF EXISTS idx_search_quantity",
"CREATE INDEX idx_search_quantity ON search_index(tenant_id, resource_type, param_name, value_quantity_value, value_quantity_unit) WHERE value_quantity_value IS NOT NULL",
"DROP INDEX IF EXISTS idx_search_reference",
"CREATE INDEX idx_search_reference ON search_index(tenant_id, resource_type, param_name, value_reference) WHERE value_reference IS NOT NULL",
"DROP INDEX IF EXISTS idx_search_uri",
"CREATE INDEX idx_search_uri ON search_index(tenant_id, resource_type, param_name, value_uri) WHERE value_uri IS NOT NULL",
"DROP INDEX IF EXISTS idx_search_token_display",
"CREATE INDEX idx_search_token_display ON search_index(tenant_id, resource_type, param_name, value_token_display) WHERE value_token_display IS NOT NULL",
"DROP INDEX IF EXISTS idx_search_identifier_type",
"CREATE INDEX idx_search_identifier_type ON search_index(tenant_id, resource_type, param_name, value_identifier_type_system, value_identifier_type_code) WHERE value_identifier_type_system IS NOT NULL OR value_identifier_type_code IS NOT NULL",
];
for sql in &statements {
conn.execute(sql, [])
.map_err(|e| migration_err(format!("v20 partial index rebuild: {e}")))?;
}
Ok(())
}

/// Drop all tables (for testing).
#[cfg(test)]
#[allow(dead_code)]
Expand Down
Loading