use super::*;
fn open_memory() -> Connection {
Connection::open_in_memory().expect("in-memory connection")
}
#[test]
fn fresh_db_migrates_to_latest() {
let mut conn = open_memory();
let version = run_migrations(&mut conn).expect("migrations should succeed");
assert_eq!(version, 22);
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22)",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(count, 22);
let tbl_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='entities'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(tbl_count, 1);
let col_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('notes') WHERE name = 'name'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(col_count, 1, "V2 must add name column to notes");
let et_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('entities') WHERE name = 'entity_type'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(et_count, 1, "V5 must add entity_type column to entities");
let idx_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' \
AND name='idx_entities_kind_entity_type'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(idx_count, 1, "V5 must create idx_entities_kind_entity_type");
let status_col: i64 = conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('notes') WHERE name = 'status'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(status_col, 1, "V10 must add status column to notes");
let merged_into_col: i64 = conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('entities') WHERE name = 'merged_into'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
merged_into_col, 1,
"V11 must add merged_into column to entities"
);
let salience_notnull: i64 = conn
.query_row(
"SELECT \"notnull\" FROM pragma_table_info('notes') WHERE name = 'salience'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(salience_notnull, 0, "V12 must make salience nullable");
for col in [
"kind",
"payload",
"payload_schema_version",
"profile_state_version",
"session_id",
"aggregate_kind",
"aggregate_id",
] {
let exists: bool = conn
.query_row(
"SELECT COUNT(*) > 0 FROM pragma_table_info('events') WHERE name = ?1",
[col],
|r| r.get(0),
)
.unwrap();
assert!(exists, "V13 must add events.{col}");
}
let obs_tbl: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='event_observations'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(obs_tbl, 1, "V13 must create event_observations table");
for idx in [
"idx_events_ns_created_id",
"idx_events_session",
"idx_events_payload_proposal_id",
"idx_event_obs_entity",
"idx_event_obs_event_role",
] {
let exists: bool = conn
.query_row(
"SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='index' AND name=?1",
[idx],
|r| r.get(0),
)
.unwrap();
assert!(exists, "V13 must create index {idx}");
}
let embed_tbl: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='_embedding_models'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(embed_tbl, 1, "V14 must create _embedding_models table");
for idx in [
"idx_embed_models_one_active",
"idx_embed_models_engine_status",
] {
let exists: bool = conn
.query_row(
"SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='index' AND name=?1",
[idx],
|r| r.get(0),
)
.unwrap();
assert!(exists, "V14 must create index {idx}");
}
let proposals_tbl: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='proposals_open'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(proposals_tbl, 1, "V15 must create proposals_open table");
for idx in [
"idx_proposals_open_ns_status",
"idx_proposals_open_proposer",
"idx_proposals_open_updated",
] {
let exists: bool = conn
.query_row(
"SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='index' AND name=?1",
[idx],
|r| r.get(0),
)
.unwrap();
assert!(exists, "V15 must create index {idx}");
}
let snap_tbl: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='brain_profile_snapshots'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(snap_tbl, 1, "V20 must create brain_profile_snapshots table");
let log_tbl: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='brain_event_log'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(log_tbl, 1, "V20 must create brain_event_log table");
let sections_tbl: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='knowledge_sections'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(sections_tbl, 1, "V21 must create knowledge_sections table");
for idx in [
"idx_knowledge_sections_atom",
"idx_knowledge_sections_ns_type",
"idx_knowledge_sections_ns_atom",
] {
let exists: bool = conn
.query_row(
"SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='index' AND name=?1",
[idx],
|r| r.get(0),
)
.unwrap();
assert!(exists, "V21 must create index {idx}");
}
for col in [
"id",
"atom_id",
"namespace",
"section_type",
"heading",
"content",
"tokens",
"sort_order",
"embedding",
"created_at",
"updated_at",
] {
let exists: bool = conn
.query_row(
"SELECT COUNT(*) > 0 FROM pragma_table_info('knowledge_sections') WHERE name = ?1",
[col],
|r| r.get(0),
)
.unwrap();
assert!(exists, "V21 must add knowledge_sections.{col}");
}
}
#[test]
fn run_migrations_twice_is_idempotent() {
let mut conn = open_memory();
let v1 = run_migrations(&mut conn).expect("first run");
let v2 = run_migrations(&mut conn).expect("second run");
assert_eq!(v1, 22);
assert_eq!(v2, 22);
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM _schema_migrations", [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(count, 22);
}
#[test]
fn migration_v9_adds_target_backend_index() {
let mut conn = open_memory();
let version = run_migrations(&mut conn).expect("migrations should succeed");
assert_eq!(
version, 22,
"F052: latest migration must be V22 (knowledge_lifecycle_status)"
);
let col: i64 = conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('graph_edges') WHERE name = 'target_backend'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
col, 1,
"F052: graph_edges must have target_backend column after V9 migration"
);
let idx: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_graph_edges_target_backend'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
idx, 1,
"F052: idx_graph_edges_target_backend partial index must exist after V9 migration"
);
}
#[test]
fn failed_migration_rolls_back() {
let bad_v23 = VersionedMigration {
version: 23,
name: "bad_migration",
up: "THIS IS NOT VALID SQL;",
};
let mut conn = open_memory();
run_migrations(&mut conn).expect("V1..V22 should apply cleanly");
let result = apply_single_migration(&mut conn, &bad_v23);
assert!(result.is_err(), "bad migration should return error");
let v23_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 23",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(v23_count, 0, "V23 must not be recorded after rollback");
let applied_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22)",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
applied_count, 22,
"V1..V22 must still be recorded after V23 rollback"
);
}
#[test]
fn store_ddl_then_migrations_is_idempotent() {
use crate::stores::entity::ensure_entities_schema;
use crate::stores::note::ensure_notes_schema;
let mut conn = open_memory();
ensure_notes_schema(&conn).expect("store DDL should create notes");
ensure_entities_schema(&conn).expect("store DDL should create entities");
let has_name: bool = conn
.query_row(
"SELECT COUNT(*) > 0 FROM pragma_table_info('notes') WHERE name = 'name'",
[],
|row| row.get(0),
)
.unwrap();
assert!(has_name, "NOTES_DDL should include name column");
let version = run_migrations(&mut conn).expect("migrations after store DDL");
assert_eq!(version, 22);
let v2_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 2",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
v2_count, 1,
"V2 must be recorded even when column pre-exists"
);
let v5_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 5",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
v5_count, 1,
"V5 must be recorded even when entity_type column pre-exists"
);
let v9_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 9",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
v9_count, 1,
"V9 must be recorded after store-DDL + migrations"
);
let v10_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 10",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
v10_count, 1,
"V10 must be recorded even when status column pre-exists via NOTES_DDL"
);
let v11_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 11",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
v11_count, 1,
"V11 must be recorded even when merged_into column pre-exists via ENTITIES_DDL"
);
let v12_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 12",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
v12_count, 1,
"V12 must be recorded even when salience is already nullable via NOTES_DDL"
);
let v13_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 13",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
v13_count, 1,
"V13 must be recorded after store-DDL + migrations"
);
let v14_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 14",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
v14_count, 1,
"V14 must be recorded after store-DDL + migrations"
);
let v15_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 15",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
v15_count, 1,
"V15 must be recorded after store-DDL + migrations"
);
let v21_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 21",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
v21_count, 1,
"V21 must be recorded after store-DDL + migrations"
);
}
#[test]
fn v1_to_v12_allows_null_salience() {
let mut conn = open_memory();
conn.execute_batch(MIGRATION_TRACKING_TABLE).unwrap();
conn.execute_batch(
"CREATE TABLE entities (\
id TEXT PRIMARY KEY,\
namespace TEXT NOT NULL,\
kind TEXT NOT NULL,\
name TEXT NOT NULL,\
description TEXT,\
properties TEXT,\
tags TEXT NOT NULL DEFAULT '[]',\
created_at INTEGER NOT NULL,\
updated_at INTEGER NOT NULL,\
deleted_at INTEGER\
);\
CREATE TABLE graph_edges (\
namespace TEXT NOT NULL,\
id TEXT NOT NULL,\
source_id TEXT NOT NULL,\
target_id TEXT NOT NULL,\
relation TEXT NOT NULL,\
weight REAL NOT NULL DEFAULT 1.0,\
created_at INTEGER NOT NULL,\
metadata TEXT,\
PRIMARY KEY (namespace, id)\
);\
CREATE TABLE notes (\
id TEXT PRIMARY KEY,\
namespace TEXT NOT NULL,\
kind TEXT NOT NULL,\
content TEXT NOT NULL DEFAULT '',\
salience REAL NOT NULL DEFAULT 0.5,\
decay_factor REAL NOT NULL DEFAULT 0.0,\
expires_at INTEGER,\
properties TEXT,\
created_at INTEGER NOT NULL,\
updated_at INTEGER NOT NULL,\
deleted_at INTEGER\
);\
CREATE TABLE events (\
id TEXT PRIMARY KEY,\
namespace TEXT NOT NULL,\
verb TEXT NOT NULL,\
substrate TEXT NOT NULL,\
actor TEXT NOT NULL,\
outcome TEXT NOT NULL,\
data TEXT,\
duration_us INTEGER NOT NULL DEFAULT 0,\
target_id TEXT,\
created_at INTEGER NOT NULL\
);",
)
.unwrap();
let now = chrono::Utc::now().timestamp_micros();
conn.execute(
"INSERT INTO _schema_migrations (version, name, applied_at) VALUES (1, 'initial_schema', ?1)",
rusqlite::params![now],
)
.unwrap();
let version = run_migrations(&mut conn).expect("migrations should succeed");
assert_eq!(version, 22);
let notnull: i64 = conn
.query_row(
"SELECT \"notnull\" FROM pragma_table_info('notes') WHERE name = 'salience'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(notnull, 0, "salience must be nullable after V12");
conn.execute(
"INSERT INTO notes (id, namespace, kind, status, content, created_at, updated_at) \
VALUES ('test-id', 'ns', 'observation', 'active', '', 1, 1)",
[],
)
.expect("inserting note with NULL salience must succeed after V12");
let stored_salience: Option<f64> = conn
.query_row(
"SELECT salience FROM notes WHERE id = 'test-id'",
[],
|row| row.get(0),
)
.unwrap();
assert!(
stored_salience.is_none(),
"salience must be NULL when not supplied"
);
}
#[test]
fn store_ddl_then_event_migration_is_idempotent() {
use crate::stores::event::ensure_events_schema;
let mut conn = open_memory();
ensure_events_schema(&conn).expect("store DDL should create events");
let version = run_migrations(&mut conn).expect("migrations after events store DDL");
assert_eq!(version, 22, "must reach V22 even when events DDL ran first");
let v13_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 13",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(v13_count, 1, "V13 must be recorded");
let v14_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 14",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(v14_count, 1, "V14 must be recorded");
let v15_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 15",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(v15_count, 1, "V15 must be recorded");
}
#[test]
fn migration_v14_creates_embedding_model_registry() {
let mut conn = open_memory();
let version = run_migrations(&mut conn).expect("migrations should succeed");
assert_eq!(
version, 22,
"F227: latest migration must be V22 (knowledge_lifecycle_status)"
);
let tbl: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='_embedding_models'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(tbl, 1, "F227: _embedding_models table must exist after V14");
let one_active_idx: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_embed_models_one_active'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(
one_active_idx, 1,
"V14 must create idx_embed_models_one_active partial unique index"
);
let engine_status_idx: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_embed_models_engine_status'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(
engine_status_idx, 1,
"V14 must create idx_embed_models_engine_status index"
);
for col in [
"id",
"engine_name",
"model_id",
"key_version",
"dim",
"output_dim",
"status",
"activated_at",
"superseded_at",
"superseded_by",
"canonical_key",
"created_at",
] {
let exists: bool = conn
.query_row(
"SELECT COUNT(*) > 0 FROM pragma_table_info('_embedding_models') WHERE name = ?1",
[col],
|r| r.get(0),
)
.unwrap();
assert!(
exists,
"F227: _embedding_models must have column '{col}' after V14"
);
}
}
#[test]
fn migration_v14_adds_embedding_model_id_to_existing_regular_vec_tables() {
let mut conn = open_memory();
conn.execute_batch(
"CREATE TABLE vec_legacy_model (\
subject_id TEXT PRIMARY KEY,\
namespace TEXT NOT NULL,\
kind TEXT NOT NULL,\
field TEXT NOT NULL\
);",
)
.unwrap();
let version = run_migrations(&mut conn).expect("migrations should succeed");
assert_eq!(version, 22);
let col_exists: bool = conn
.query_row(
"SELECT COUNT(*) > 0 FROM pragma_table_info('vec_legacy_model') WHERE name = 'embedding_model_id'",
[],
|r| r.get(0),
)
.unwrap();
assert!(
col_exists,
"F228: V14 must add embedding_model_id to existing regular vec_ tables"
);
let version2 = run_migrations(&mut conn).expect("second run must succeed");
assert_eq!(version2, 22);
}
#[test]
fn migration_v14_does_not_alter_sqlite_vec_shadow_tables() {
let mut conn = open_memory();
conn.execute_batch(
"CREATE TABLE vec_test_chunks (x INTEGER);\
CREATE TABLE vec_test_rowids (x INTEGER);\
CREATE TABLE vec_test_info (x INTEGER);\
CREATE TABLE vec_test_vector_chunks00 (x INTEGER);",
)
.unwrap();
let version = run_migrations(&mut conn).expect("migrations should succeed");
assert_eq!(version, 22);
for shadow in [
"vec_test_chunks",
"vec_test_rowids",
"vec_test_info",
"vec_test_vector_chunks00",
] {
let col_added: bool = conn
.query_row(
"SELECT COUNT(*) > 0 FROM pragma_table_info(?1) \
WHERE name = 'embedding_model_id'",
rusqlite::params![shadow],
|r| r.get(0),
)
.unwrap();
assert!(
!col_added,
"CRIT-2: V14 must NOT add embedding_model_id to sqlite-vec shadow table '{shadow}'"
);
}
}
#[test]
fn v17_preserving_rebuild_preserves_rows_and_infers_model() {
let conn = open_memory();
conn.execute_batch(MIGRATION_TRACKING_TABLE).unwrap();
assert_eq!(
infer_model_from_table_name("vec_paraphrase"),
"paraphrase",
"suffix 'paraphrase' should be returned as-is"
);
assert_eq!(
infer_model_from_table_name("vec_all_minilm_l6_v2"),
"all_minilm_l6_v2",
"underscore-containing suffix should be returned as-is"
);
conn.execute_batch(
"CREATE TABLE vec_paraphrase (\
subject_id TEXT PRIMARY KEY,\
namespace TEXT NOT NULL,\
kind TEXT NOT NULL,\
embedding BLOB NOT NULL\
);",
)
.unwrap();
conn.execute_batch(
"INSERT INTO vec_paraphrase (subject_id, namespace, kind, embedding) \
VALUES ('id-1', 'ns', 'entity', X'0000803F');",
)
.unwrap();
conn.execute_batch(
"CREATE TABLE tmp_vec_paraphrase (\
subject_id TEXT PRIMARY KEY,\
namespace TEXT NOT NULL,\
kind TEXT NOT NULL,\
field TEXT NOT NULL,\
embedding_model TEXT NOT NULL,\
embedding BLOB NOT NULL\
);\
INSERT INTO tmp_vec_paraphrase \
(subject_id, namespace, kind, field, embedding_model, embedding) \
SELECT subject_id, namespace, kind, '' AS field, 'paraphrase' AS embedding_model, embedding \
FROM vec_paraphrase;\
DROP TABLE vec_paraphrase;\
CREATE TABLE vec_paraphrase (\
subject_id TEXT PRIMARY KEY,\
namespace TEXT NOT NULL,\
kind TEXT NOT NULL,\
field TEXT NOT NULL,\
embedding_model TEXT NOT NULL,\
embedding BLOB NOT NULL\
);\
INSERT INTO vec_paraphrase \
(subject_id, namespace, kind, field, embedding_model, embedding) \
SELECT subject_id, namespace, kind, field, embedding_model, embedding \
FROM tmp_vec_paraphrase;\
DROP TABLE tmp_vec_paraphrase;",
)
.unwrap();
let (ns, model): (String, String) = conn
.query_row(
"SELECT namespace, embedding_model FROM vec_paraphrase WHERE subject_id = 'id-1'",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
assert_eq!(ns, "ns");
assert_eq!(
model, "paraphrase",
"V17 must infer model 'paraphrase' from table name 'vec_paraphrase'"
);
}
#[test]
fn v17_infer_model_known_suffix() {
assert_eq!(infer_model_from_table_name("vec_paraphrase"), "paraphrase");
}
#[test]
fn v17_infer_model_fallback_for_unknown_suffix() {
assert_eq!(
infer_model_from_table_name("vec_"),
"all-minilm-l6-v2",
"empty suffix must fall back to all-minilm-l6-v2"
);
assert_eq!(
infer_model_from_table_name("other_table"),
"all-minilm-l6-v2",
"non-vec_ prefix must fall back to all-minilm-l6-v2"
);
}
#[test]
fn v17_migration_is_noop_on_fresh_db() {
let mut conn = open_memory();
let version = run_migrations(&mut conn).expect("migrations must succeed on fresh DB");
assert_eq!(version, 22);
let v17: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 17",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(v17, 1, "V17 must be recorded on fresh DB");
let v18: i64 = conn
.query_row(
"SELECT COUNT(*) FROM _schema_migrations WHERE version = 18",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(v18, 1, "V18 must be recorded on fresh DB");
}
#[test]
fn v17_skips_tables_that_already_have_both_columns() {
let conn = open_memory();
conn.execute_batch(
"CREATE TABLE vec_modern (\
subject_id TEXT PRIMARY KEY,\
namespace TEXT NOT NULL,\
kind TEXT NOT NULL,\
field TEXT NOT NULL,\
embedding_model TEXT NOT NULL DEFAULT 'all-minilm-l6-v2',\
embedding BLOB NOT NULL\
);\
INSERT INTO vec_modern VALUES ('id-2', 'ns', 'entity', 'content', 'my-model', X'00');",
)
.unwrap();
let sql = build_v17_preserving_rebuild_sql(&conn).unwrap();
assert_eq!(
sql, "SELECT 1;",
"V17 must produce no-op SQL when no vec0 virtual tables need rebuilding"
);
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM vec_modern", [], |r| r.get(0))
.unwrap();
assert_eq!(count, 1);
}
fn apply_single_migration(
conn: &mut Connection,
migration: &VersionedMigration,
) -> Result<(), SqliteError> {
let tx = conn.transaction().map_err(|e| SqliteError::Migration {
version: migration.version,
error: e.to_string(),
})?;
tx.execute_batch(migration.up)
.map_err(|e| SqliteError::Migration {
version: migration.version,
error: e.to_string(),
})?;
let now = chrono::Utc::now().timestamp_micros();
tx.execute(
"INSERT INTO _schema_migrations (version, name, applied_at) VALUES (?1, ?2, ?3)",
rusqlite::params![migration.version, migration.name, now],
)
.map_err(|e| SqliteError::Migration {
version: migration.version,
error: e.to_string(),
})?;
tx.commit().map_err(|e| SqliteError::Migration {
version: migration.version,
error: e.to_string(),
})?;
Ok(())
}