Files
mtgc/crates/mtgc-core/src/db.rs
garrettmills 399c628461 Initial commit: MTG personal library catalog (Tauri v2 + React/TS)
Full working app with:
- Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB
  architecture (reference.db + library.db via ATTACH), criteria filters, deck ops
- Tauri v2 shell exposing the core as typed IPC commands
- React/TS frontend: card entry, library browser, deck editor
- Features: fuzzy card search, color/CMC/type/commander-identity filters,
  tile/list view toggle, hover card previews, deck format + commander support,
  per-card location field, grouping by type/subtype/CMC/color, localStorage
  persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:37:56 -05:00

119 lines
4.1 KiB
Rust

//! Database wiring: open the library, run migrations, ATTACH the reference db.
use std::path::Path;
use rusqlite::Connection;
use crate::error::Result;
/// The reference schema, applied to a freshly-created reference.db by the ingester
/// (and by tests that build a fixture reference db).
pub const REFERENCE_SCHEMA: &str = include_str!("sql/reference_schema.sql");
/// Ordered library migrations. Index + 1 is the schema version recorded in
/// `PRAGMA user_version`. Append new migrations; never edit an applied one.
const LIBRARY_MIGRATIONS: &[&str] = &[
include_str!("sql/library/0001_init.sql"),
include_str!("sql/library/0002_location.sql"),
include_str!("sql/library/0003_deck_format.sql"),
];
/// The attached alias under which the reference db is available at runtime.
pub const REF_DB: &str = "ref";
/// Correlated subquery selecting one image URL for the oracle card aliased `oc`.
/// Returns NULL when no printing has an image. Read it as `Option<String>`.
pub fn image_subquery() -> String {
format!(
"(SELECT p.image_uri FROM {REF_DB}.printings p
WHERE p.oracle_id = oc.oracle_id AND p.image_uri <> '' LIMIT 1)"
)
}
/// Create (if needed) and open a reference.db, applying the reference schema.
/// Used by the ingester and by test fixtures.
pub fn open_reference_for_build(path: &Path) -> Result<Connection> {
let conn = Connection::open(path)?;
conn.execute_batch(REFERENCE_SCHEMA)?;
Ok(conn)
}
/// Open the library db at `library_path`, run migrations, and ATTACH the
/// reference db at `reference_path` under the [`REF_DB`] alias.
pub fn open(library_path: &Path, reference_path: &Path) -> Result<Connection> {
let conn = Connection::open(library_path)?;
conn.pragma_update(None, "foreign_keys", true)?;
migrate_library(&conn)?;
attach_reference(&conn, reference_path)?;
Ok(conn)
}
/// ATTACH a reference db to an already-open library connection.
pub fn attach_reference(conn: &Connection, reference_path: &Path) -> Result<()> {
conn.execute(
&format!("ATTACH DATABASE ?1 AS {REF_DB}"),
[reference_path.to_string_lossy()],
)?;
Ok(())
}
/// Apply any library migrations newer than the recorded `user_version`.
pub fn migrate_library(conn: &Connection) -> Result<()> {
let current: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
for (idx, sql) in LIBRARY_MIGRATIONS.iter().enumerate() {
let version = (idx + 1) as i64;
if version > current {
conn.execute_batch(&format!("BEGIN;\n{sql}\nPRAGMA user_version = {version};\nCOMMIT;"))?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn migrations_are_idempotent_and_bump_version() {
let conn = Connection::open_in_memory().unwrap();
migrate_library(&conn).unwrap();
let v: i64 = conn
.pragma_query_value(None, "user_version", |r| r.get(0))
.unwrap();
assert_eq!(v, LIBRARY_MIGRATIONS.len() as i64);
// Running again is a no-op (no "table already exists" error).
migrate_library(&conn).unwrap();
// Core tables exist.
let n: i64 = conn
.query_row(
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('decks','library_cards','deck_cards','library_card_copies')",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(n, 4);
}
#[test]
fn reference_schema_applies_with_fts() {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(REFERENCE_SCHEMA).unwrap();
// FTS5 + trigram tokenizer must be available in the bundled SQLite.
conn.execute(
"INSERT INTO oracle_names_fts(name, oracle_id) VALUES ('Llanowar Elves','x')",
[],
)
.unwrap();
let hit: i64 = conn
.query_row(
"SELECT count(*) FROM oracle_names_fts WHERE oracle_names_fts MATCH 'lan'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(hit, 1);
}
}