From 168c27bddffda13afccaf35661a84c5e804a41b0 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 28 Sep 2026 13:49:52 -0400 Subject: [PATCH] foreversight: Use `sea-query` for interacting with the SQLite database --- Cargo.lock | 95 +++- crates/foreversight/Cargo.toml | 7 +- crates/foreversight/src/persistence/sqlite.rs | 497 ++++++++++++------ 3 files changed, 434 insertions(+), 165 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 086af47..c8c035f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -797,6 +797,40 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + [[package]] name = "dirs" version = "7.0.0" @@ -1090,6 +1124,8 @@ dependencies = [ "dirs", "reqwest", "rusqlite", + "sea-query", + "sea-query-rusqlite", "serde", "serde_json", "shikra", @@ -1351,13 +1387,19 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.12.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ - "hashbrown 0.17.1", + "hashbrown 0.16.1", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.5.3" @@ -1603,6 +1645,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1835,9 +1883,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.38.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" dependencies = [ "cc", "pkg-config", @@ -3048,9 +3096,9 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.40.2" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" +checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ "bitflags 2.13.1", "fallible-iterator", @@ -3238,6 +3286,39 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "sea-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "546040c653a705e60ec65ecd3191a809603734bebbc225775916dea9ae409b31" +dependencies = [ + "sea-query-derive", +] + +[[package]] +name = "sea-query-derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b0f466921cdd3cf4b89d5c3ac2173dba89a873ab395b123a645de181ec7537" +dependencies = [ + "darling", + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", + "thiserror 2.0.21", +] + +[[package]] +name = "sea-query-rusqlite" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec6038023c8517c623e5bf9606b3c54d40bc8296bb6b2986040428dd84deddd" +dependencies = [ + "rusqlite", + "sea-query", +] + [[package]] name = "security-framework" version = "3.7.0" diff --git a/crates/foreversight/Cargo.toml b/crates/foreversight/Cargo.toml index 1a07ea8..b19f0fc 100644 --- a/crates/foreversight/Cargo.toml +++ b/crates/foreversight/Cargo.toml @@ -9,8 +9,11 @@ dirs = "7" # rustls rather than OpenSSL, so there are no system TLS libraries to find. reqwest = { version = "0.13", default-features = false, features = ["http2", "rustls"] } # Bundled, so there's no system SQLite to find. `fallible_uint` for `u64`s, like -# copper, which fail rather than wrap if they don't fit in an `i64`. -rusqlite = { version = "0.40", features = ["bundled", "fallible_uint"] } +# copper, which fail rather than wrap if they don't fit in an `i64`. Held at the +# version `sea-query-rusqlite` uses, since only one can link SQLite. +rusqlite = { version = "0.38", features = ["bundled", "fallible_uint"] } +sea-query = { version = "1", default-features = false, features = ["derive", "backend-sqlite"] } +sea-query-rusqlite = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" shikra = { path = "../shikra" } diff --git a/crates/foreversight/src/persistence/sqlite.rs b/crates/foreversight/src/persistence/sqlite.rs index 2965556..879a85a 100644 --- a/crates/foreversight/src/persistence/sqlite.rs +++ b/crates/foreversight/src/persistence/sqlite.rs @@ -4,64 +4,167 @@ use std::fs; use std::path::{Path, PathBuf}; use anyhow::{Context as _, Result, bail}; -use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; -use rusqlite::{Connection, Row, ToSql, params}; +use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef}; +use rusqlite::{Connection, Row}; +use sea_query::{ + ColumnDef, Expr, ExprTrait, ForeignKey, ForeignKeyAction, Iden, Index, Order, Query, + SelectStatement, SqliteQueryBuilder, Table, +}; +use sea_query_rusqlite::RusqliteBinder; use crate::domain::{ Character, CharacterId, CharacterRepository, ItemStack, Location, Profession, ProfessionKind, Quality, }; +/// The tables and their columns. +/// +/// The migrations use these too, so once one has run, don't rename them. +/// Add new ones instead, and a migration that renames the column. +mod schema { + use sea_query::Iden; + + #[derive(Debug, Clone, Copy, Iden)] + pub enum Character { + Table, + Id, + Name, + /// Empty if the character doesn't have one, so it can be part of a unique + /// index: nulls are never equal, so a character without one could be in + /// twice. + Surname, + Realm, + Class, + ClassName, + Race, + Faction, + Level, + Money, + UpdatedAt, + BankUpdatedAt, + MailUpdatedAt, + MailNotShown, + } + + #[derive(Debug, Clone, Copy, Iden)] + pub enum ItemStack { + Table, + CharacterId, + Position, + ItemId, + /// Colon-separated, like Wowhead's `?bonus=`. + BonusIds, + Name, + Quality, + Count, + Location, + ExpiresAt, + } + + #[derive(Debug, Clone, Copy, Iden)] + pub enum Profession { + Table, + CharacterId, + Position, + Name, + Kind, + Rank, + MaxRank, + } +} + /// The schema, one migration per version. Only ever add to the end: a /// database's `user_version` is how many of them it has run. -const MIGRATIONS: &[&str] = &[r#" - create table character ( - id integer primary key, - name text not null, - surname text, - realm text not null, - class text not null, - class_name text not null, - race text not null, - faction text not null, - level integer not null, - money integer not null, - updated_at integer, - bank_updated_at integer, - mail_updated_at integer, - mail_not_shown integer not null - ); - -- A plain `unique` constraint would let characters without a surname in - -- twice, since nulls are never equal. - create unique index character_by_id on character (name, ifnull(surname, ''), realm); - - create table item_stack ( - character_id integer not null references character (id) on delete cascade, - position integer not null, - item_id integer not null, - -- Colon-separated, like Wowhead's `?bonus=`. - bonus_ids text not null, - name text not null, - quality text not null, - count integer not null, - location text not null, - expires_at integer, - primary key (character_id, position) - ); - - create table profession ( - character_id integer not null references character (id) on delete cascade, - position integer not null, - name text not null, - kind text not null, - rank integer not null, - max_rank integer not null, - primary key (character_id, position) - ); -"#]; - -const CHARACTER_COLUMNS: &str = "id, name, surname, realm, class, class_name, race, faction, \ - level, money, updated_at, bank_updated_at, mail_updated_at, mail_not_shown"; +const MIGRATIONS: &[fn() -> Vec] = &[create_tables]; + +fn create_tables() -> Vec { + use schema::{Character as C, ItemStack as I, Profession as P}; + + let character = Table::create() + .table(C::Table) + .col(ColumnDef::new(C::Id).integer().primary_key()) + .col(ColumnDef::new(C::Name).text().not_null()) + .col(ColumnDef::new(C::Surname).text().not_null()) + .col(ColumnDef::new(C::Realm).text().not_null()) + .col(ColumnDef::new(C::Class).text().not_null()) + .col(ColumnDef::new(C::ClassName).text().not_null()) + .col(ColumnDef::new(C::Race).text().not_null()) + .col(ColumnDef::new(C::Faction).text().not_null()) + .col(ColumnDef::new(C::Level).integer().not_null()) + .col(ColumnDef::new(C::Money).integer().not_null()) + .col(ColumnDef::new(C::UpdatedAt).integer().null()) + .col(ColumnDef::new(C::BankUpdatedAt).integer().null()) + .col(ColumnDef::new(C::MailUpdatedAt).integer().null()) + .col(ColumnDef::new(C::MailNotShown).integer().not_null()) + .to_string(SqliteQueryBuilder); + let character_by_id = Index::create() + .name("character_by_id") + .table(C::Table) + .col(C::Name) + .col(C::Surname) + .col(C::Realm) + .unique() + .to_string(SqliteQueryBuilder); + + let item_stack = Table::create() + .table(I::Table) + .col(ColumnDef::new(I::CharacterId).integer().not_null()) + .col(ColumnDef::new(I::Position).integer().not_null()) + .col(ColumnDef::new(I::ItemId).integer().not_null()) + .col(ColumnDef::new(I::BonusIds).text().not_null()) + .col(ColumnDef::new(I::Name).text().not_null()) + .col(ColumnDef::new(I::Quality).text().not_null()) + .col(ColumnDef::new(I::Count).integer().not_null()) + .col(ColumnDef::new(I::Location).text().not_null()) + .col(ColumnDef::new(I::ExpiresAt).integer().null()) + .primary_key(Index::create().col(I::CharacterId).col(I::Position)) + .foreign_key( + ForeignKey::create() + .from(I::Table, I::CharacterId) + .to(C::Table, C::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_string(SqliteQueryBuilder); + + let profession = Table::create() + .table(P::Table) + .col(ColumnDef::new(P::CharacterId).integer().not_null()) + .col(ColumnDef::new(P::Position).integer().not_null()) + .col(ColumnDef::new(P::Name).text().not_null()) + .col(ColumnDef::new(P::Kind).text().not_null()) + .col(ColumnDef::new(P::Rank).integer().not_null()) + .col(ColumnDef::new(P::MaxRank).integer().not_null()) + .primary_key(Index::create().col(P::CharacterId).col(P::Position)) + .foreign_key( + ForeignKey::create() + .from(P::Table, P::CharacterId) + .to(C::Table, C::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_string(SqliteQueryBuilder); + + vec![character, character_by_id, item_stack, profession] +} + +const CHARACTER_COLUMNS: [schema::Character; 14] = { + use schema::Character as C; + [ + C::Id, + C::Name, + C::Surname, + C::Realm, + C::Class, + C::ClassName, + C::Race, + C::Faction, + C::Level, + C::Money, + C::UpdatedAt, + C::BankUpdatedAt, + C::MailUpdatedAt, + C::MailNotShown, + ] +}; pub struct SqliteCharacterRepository { connection: Connection, @@ -99,155 +202,235 @@ impl SqliteCharacterRepository { /// Reads a row of [`CHARACTER_COLUMNS`], then the character's items and /// professions. fn character(&self, row: &Row) -> Result { - let row_id: i64 = row.get("id")?; + use schema::Character as C; + + let row_id: i64 = get(row, C::Id)?; + let surname: String = get(row, C::Surname)?; Ok(Character { id: CharacterId { - name: row.get("name")?, - surname: row.get("surname")?, - realm: row.get("realm")?, + name: get(row, C::Name)?, + surname: (!surname.is_empty()).then_some(surname), + realm: get(row, C::Realm)?, }, - class: row.get("class")?, - class_name: row.get("class_name")?, - race: row.get("race")?, - faction: row.get("faction")?, - level: row.get("level")?, - money: row.get("money")?, - updated_at: row.get("updated_at")?, - bank_updated_at: row.get("bank_updated_at")?, - mail_updated_at: row.get("mail_updated_at")?, - mail_not_shown: row.get("mail_not_shown")?, + class: get(row, C::Class)?, + class_name: get(row, C::ClassName)?, + race: get(row, C::Race)?, + faction: get(row, C::Faction)?, + level: get(row, C::Level)?, + money: get(row, C::Money)?, + updated_at: get(row, C::UpdatedAt)?, + bank_updated_at: get(row, C::BankUpdatedAt)?, + mail_updated_at: get(row, C::MailUpdatedAt)?, + mail_not_shown: get(row, C::MailNotShown)?, items: self.item_stacks(row_id)?, professions: self.professions(row_id)?, }) } fn item_stacks(&self, character_id: i64) -> Result> { - let mut statement = self.connection.prepare_cached( - "select item_id, bonus_ids, name, quality, count, location, expires_at - from item_stack where character_id = ?1 order by position", - )?; - let stacks = statement.query_map([character_id], |row| { - let bonus_ids: String = row.get("bonus_ids")?; + use schema::ItemStack as I; + + let (sql, values) = Query::select() + .columns([ + I::ItemId, + I::BonusIds, + I::Name, + I::Quality, + I::Count, + I::Location, + I::ExpiresAt, + ]) + .from(I::Table) + .and_where(Expr::col(I::CharacterId).eq(character_id)) + .order_by(I::Position, Order::Asc) + .build_rusqlite(SqliteQueryBuilder); + let mut statement = self.connection.prepare_cached(&sql)?; + let stacks = statement.query_map(&*values.as_params(), |row| { + let bonus_ids: String = get(row, I::BonusIds)?; Ok(ItemStack { - item_id: row.get("item_id")?, + item_id: get(row, I::ItemId)?, bonus_ids: decode_bonus_ids(&bonus_ids), - name: row.get("name")?, - quality: row.get("quality")?, - count: row.get("count")?, - location: row.get("location")?, - expires_at: row.get("expires_at")?, + name: get(row, I::Name)?, + quality: get(row, I::Quality)?, + count: get(row, I::Count)?, + location: get(row, I::Location)?, + expires_at: get(row, I::ExpiresAt)?, }) })?; Ok(stacks.collect::>()?) } fn professions(&self, character_id: i64) -> Result> { - let mut statement = self.connection.prepare_cached( - "select name, kind, rank, max_rank - from profession where character_id = ?1 order by position", - )?; - let professions = statement.query_map([character_id], |row| { + use schema::Profession as P; + + let (sql, values) = Query::select() + .columns([P::Name, P::Kind, P::Rank, P::MaxRank]) + .from(P::Table) + .and_where(Expr::col(P::CharacterId).eq(character_id)) + .order_by(P::Position, Order::Asc) + .build_rusqlite(SqliteQueryBuilder); + let mut statement = self.connection.prepare_cached(&sql)?; + let professions = statement.query_map(&*values.as_params(), |row| { Ok(Profession { - name: row.get("name")?, - kind: row.get("kind")?, - rank: row.get("rank")?, - max_rank: row.get("max_rank")?, + name: get(row, P::Name)?, + kind: get(row, P::Kind)?, + rank: get(row, P::Rank)?, + max_rank: get(row, P::MaxRank)?, }) })?; Ok(professions.collect::>()?) } -} -impl CharacterRepository for SqliteCharacterRepository { - fn all(&self) -> Result> { - let mut statement = self.connection.prepare_cached(&format!( - "select {CHARACTER_COLUMNS} from character order by id" - ))?; - let mut rows = statement.query([])?; + fn select_characters(&self, query: &SelectStatement) -> Result> { + let (sql, values) = query.build_rusqlite(SqliteQueryBuilder); + let mut statement = self.connection.prepare_cached(&sql)?; + let mut rows = statement.query(&*values.as_params())?; let mut characters = Vec::new(); while let Some(row) = rows.next()? { characters.push(self.character(row)?); } Ok(characters) } +} + +impl CharacterRepository for SqliteCharacterRepository { + fn all(&self) -> Result> { + use schema::Character as C; + + self.select_characters( + Query::select() + .columns(CHARACTER_COLUMNS) + .from(C::Table) + .order_by(C::Id, Order::Asc), + ) + } fn get(&self, id: &CharacterId) -> Result> { - let mut statement = self.connection.prepare_cached(&format!( - "select {CHARACTER_COLUMNS} from character - where name = ?1 and surname is ?2 and realm = ?3" - ))?; - let mut rows = statement.query(params![id.name, id.surname, id.realm])?; - rows.next()?.map(|row| self.character(row)).transpose() + use schema::Character as C; + + let characters = self.select_characters( + Query::select() + .columns(CHARACTER_COLUMNS) + .from(C::Table) + .and_where(Expr::col(C::Name).eq(&id.name)) + .and_where(Expr::col(C::Surname).eq(surname(id))) + .and_where(Expr::col(C::Realm).eq(&id.realm)), + )?; + Ok(characters.into_iter().next()) } fn save(&mut self, character: &Character) -> Result<()> { + use schema::{Character as C, ItemStack as I, Profession as P}; + let transaction = self.connection.transaction()?; let id = &character.id; // Replacing the row deletes the old items and professions with it. - transaction.execute( - "delete from character where name = ?1 and surname is ?2 and realm = ?3", - params![id.name, id.surname, id.realm], + execute( + &transaction, + Query::delete() + .from_table(C::Table) + .and_where(Expr::col(C::Name).eq(&id.name)) + .and_where(Expr::col(C::Surname).eq(surname(id))) + .and_where(Expr::col(C::Realm).eq(&id.realm)), )?; - transaction.execute( - "insert into character (name, surname, realm, class, class_name, race, faction, - level, money, updated_at, bank_updated_at, mail_updated_at, mail_not_shown) - values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", - params![ - id.name, - id.surname, - id.realm, - character.class, - character.class_name, - character.race, - character.faction, - character.level, - character.money, - character.updated_at, - character.bank_updated_at, - character.mail_updated_at, - character.mail_not_shown, - ], + execute( + &transaction, + Query::insert() + .into_table(C::Table) + .columns(CHARACTER_COLUMNS.into_iter().skip(1)) + .values([ + id.name.clone().into(), + surname(id).into(), + id.realm.clone().into(), + character.class.clone().into(), + character.class_name.clone().into(), + character.race.clone().into(), + character.faction.clone().into(), + character.level.into(), + character.money.into(), + character.updated_at.into(), + character.bank_updated_at.into(), + character.mail_updated_at.into(), + character.mail_not_shown.into(), + ])?, )?; let row_id = transaction.last_insert_rowid(); - { - let mut insert = transaction.prepare_cached( - "insert into item_stack (character_id, position, item_id, bonus_ids, name, - quality, count, location, expires_at) - values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - )?; - for (position, stack) in character.items.iter().enumerate() { - insert.execute(params![ - row_id, - position, - stack.item_id, - encode_bonus_ids(&stack.bonus_ids), - stack.name, - stack.quality, - stack.count, - stack.location, - stack.expires_at, + + if !character.items.is_empty() { + let mut insert = Query::insert(); + insert.into_table(I::Table).columns([ + I::CharacterId, + I::Position, + I::ItemId, + I::BonusIds, + I::Name, + I::Quality, + I::Count, + I::Location, + I::ExpiresAt, + ]); + for (position, stack) in (0_i64..).zip(&character.items) { + insert.values([ + row_id.into(), + position.into(), + stack.item_id.into(), + encode_bonus_ids(&stack.bonus_ids).into(), + stack.name.clone().into(), + stack.quality.into(), + stack.count.into(), + stack.location.into(), + stack.expires_at.into(), ])?; } - let mut insert = transaction.prepare_cached( - "insert into profession (character_id, position, name, kind, rank, max_rank) - values (?1, ?2, ?3, ?4, ?5, ?6)", - )?; - for (position, profession) in character.professions.iter().enumerate() { - insert.execute(params![ - row_id, - position, - profession.name, - profession.kind, - profession.rank, - profession.max_rank, + execute(&transaction, &insert)?; + } + + if !character.professions.is_empty() { + let mut insert = Query::insert(); + insert.into_table(P::Table).columns([ + P::CharacterId, + P::Position, + P::Name, + P::Kind, + P::Rank, + P::MaxRank, + ]); + for (position, profession) in (0_i64..).zip(&character.professions) { + insert.values([ + row_id.into(), + position.into(), + profession.name.clone().into(), + profession.kind.into(), + profession.rank.into(), + profession.max_rank.into(), ])?; } + execute(&transaction, &insert)?; } + transaction.commit()?; Ok(()) } } +/// How a character's surname is stored. See [`schema::Character::Surname`]. +fn surname(id: &CharacterId) -> &str { + id.surname.as_deref().unwrap_or_default() +} + +fn execute(connection: &Connection, statement: &impl RusqliteBinder) -> Result<()> { + let (sql, values) = statement.build_rusqlite(SqliteQueryBuilder); + connection + .prepare_cached(&sql)? + .execute(&*values.as_params())?; + Ok(()) +} + +/// Reads `column` from a row. +fn get(row: &Row, column: impl Iden) -> rusqlite::Result { + row.get(column.to_string().as_str()) +} + /// Runs the migrations the database hasn't run yet. fn migrate(connection: &mut Connection) -> Result<()> { let version: usize = connection.pragma_query_value(None, "user_version", |row| row.get(0))?; @@ -259,9 +442,11 @@ fn migrate(connection: &mut Connection) -> Result<()> { } let transaction = connection.transaction()?; for (index, migration) in MIGRATIONS.iter().enumerate().skip(version) { - transaction - .execute_batch(migration) - .with_context(|| format!("migrating the database to version {}", index + 1))?; + for statement in migration() { + transaction + .execute(&statement, []) + .with_context(|| format!("migrating the database to version {}", index + 1))?; + } } transaction.pragma_update(None, "user_version", MIGRATIONS.len())?; transaction.commit()?; @@ -305,13 +490,13 @@ const PROFESSION_KINDS: &[(ProfessionKind, &str)] = &[ macro_rules! sql_enum { ($type:ty, $names:expr) => { - impl ToSql for $type { - fn to_sql(&self) -> rusqlite::Result> { + impl From<$type> for sea_query::Value { + fn from(value: $type) -> Self { let (_, name) = $names .iter() - .find(|(value, _)| value == self) + .find(|(v, _)| *v == value) .expect("every value has a name"); - Ok(ToSqlOutput::from(*name)) + (*name).into() } } -- knotmirror 0.0.1