pub(super) const CACHE_KEY_PREFIX_MERKLE: u8 = 0x01;
pub(super) const CACHE_KEY_PREFIX_SESSION: u8 = 0x02;
pub(super) const CACHE_KEY_PREFIX_REPLAY_NULLIFIER: u8 = 0x03;
use walletkit_sqlite::{params, Connection, DbResult};
const MIGRATIONS: &[fn(&Connection) -> DbResult<()>] =
&[migration_v1, migration_v2, migration_v3];
#[allow(
clippy::cast_possible_wrap,
reason = "the migration list cannot approach i64::MAX"
)]
const SCHEMA_VERSION: i64 = MIGRATIONS.len() as i64;
pub(super) fn ensure_schema(conn: &Connection) -> DbResult<()> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS cache_meta (
schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);",
)?;
let existing = conn.query_row_optional(
"SELECT schema_version FROM cache_meta LIMIT 1;",
&[],
|stmt| Ok(stmt.column_i64(0)),
)?;
match existing {
Some(version) if version == SCHEMA_VERSION => {
if !schema_is_intact(conn)? {
reset_schema(conn)?;
}
}
Some(version) if version < SCHEMA_VERSION => {
migrate(conn, version)?;
}
Some(_) => {
reset_schema(conn)?;
}
None => {
migrate(conn, 0)?;
}
}
Ok(())
}
fn migrate(conn: &Connection, from: i64) -> DbResult<()> {
let applied = usize::try_from(from).unwrap_or(0);
for migration in MIGRATIONS.iter().skip(applied) {
migration(conn)?;
}
record_version(conn)
}
fn reset_schema(conn: &Connection) -> DbResult<()> {
conn.execute_batch(
"DROP TABLE IF EXISTS used_nullifiers;
DROP TABLE IF EXISTS merkle_proof_cache;
DROP TABLE IF EXISTS session_keys;
DROP TABLE IF EXISTS activity_entries;
DROP TABLE IF EXISTS cache_entries;",
)?;
migrate(conn, 0)
}
fn schema_is_intact(conn: &Connection) -> DbResult<bool> {
let present = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master
WHERE type = 'table' AND name IN ('cache_entries', 'activity_entries');",
&[],
|stmt| Ok(stmt.column_i64(0)),
)?;
Ok(present == 2)
}
fn record_version(conn: &Connection) -> DbResult<()> {
let updated = conn.execute(
"UPDATE cache_meta
SET schema_version = ?1, updated_at = strftime('%s','now')",
params![SCHEMA_VERSION],
)?;
if updated == 0 {
conn.execute(
"INSERT INTO cache_meta (schema_version, created_at, updated_at)
VALUES (?1, strftime('%s','now'), strftime('%s','now'))",
params![SCHEMA_VERSION],
)?;
}
Ok(())
}
fn migration_v1(conn: &Connection) -> DbResult<()> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS cache_entries (
key_bytes BLOB NOT NULL,
value_bytes BLOB NOT NULL,
inserted_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
PRIMARY KEY (key_bytes)
);
CREATE INDEX IF NOT EXISTS idx_cache_entries_expiry
ON cache_entries (expires_at);",
)
}
fn migration_v2(conn: &Connection) -> DbResult<()> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS activity_entries (
entry_id INTEGER PRIMARY KEY,
client_id TEXT NOT NULL,
protocol INTEGER NOT NULL,
created_at INTEGER NOT NULL,
outcome TEXT NOT NULL,
app_identifier TEXT NOT NULL,
issuer_schema_ids BLOB NOT NULL,
failure_reason TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_activity_entries_created_at
ON activity_entries (created_at DESC);",
)
}
fn migration_v3(conn: &Connection) -> DbResult<()> {
conn.execute_batch(
"DROP TABLE IF EXISTS activity_entries;
DROP TABLE IF EXISTS activity_meta;
CREATE TABLE activity_entries (
entry_id INTEGER PRIMARY KEY,
client_id TEXT NOT NULL,
protocol INTEGER NOT NULL,
created_at INTEGER NOT NULL,
outcome TEXT NOT NULL,
rp_id INTEGER NOT NULL,
app_identifier TEXT NOT NULL,
issuer_schema_ids BLOB NOT NULL,
failure_reason TEXT NOT NULL
);
CREATE INDEX idx_activity_entries_created_at
ON activity_entries (created_at DESC);",
)
}