rust-query 0.9.1

A query builder using rust concepts.
Documentation
use std::{collections::HashMap, fs};

use rust_query::{
    Database, Lazy,
    migration::{Config, Migrated, schema},
};

pub use v2::*;

#[schema(Schema)]
#[version(0..=2)]
pub mod vN {
    use jiff::civil::Date;
    use rust_query::TableRow;

    pub struct Album {
        pub title: String,
        #[index]
        pub artist: TableRow<Artist>,
    }
    pub struct Artist {
        #[unique]
        pub name: String,
    }
    pub struct Customer {
        #[version(..2)]
        pub phone: Option<String>,
        #[version(2..)]
        pub phone: Option<i64>,
        pub first_name: String,
        pub last_name: String,
        pub company: Option<String>,
        pub address: String,
        pub city: String,
        pub state: Option<String>,
        pub country: String,
        pub postal_code: Option<String>,
        pub fax: Option<String>,
        #[unique]
        pub email: String,
        pub support_rep: TableRow<Employee>,
    }
    #[version(1..)]
    #[unique(employee, artist)]
    pub struct ListensTo {
        pub employee: TableRow<Employee>,
        pub artist: TableRow<Artist>,
    }
    pub struct Employee {
        pub last_name: String,
        pub first_name: String,
        pub title: Option<String>,
        pub reports_to: Option<TableRow<Employee>>,
        pub birth_date: Date,
        pub hire_date: Date,
        pub address: Option<String>,
        pub city: Option<String>,
        pub state: Option<String>,
        pub country: Option<String>,
        pub postal_code: Option<String>,
        pub phone: Option<String>,
        pub fax: Option<String>,
        #[version(..2)]
        pub email: String,
    }
    pub struct Genre {
        #[unique]
        pub name: String,
    }
    #[version(1..)]
    #[from(Genre)]
    pub struct GenreNew {
        pub name: String,
        #[version(2..)]
        pub extra: i64,
    }
    #[version(1..)]
    #[from(Genre)]
    pub struct ShortGenre {
        pub name: String,
    }
    pub struct Invoice {
        #[index]
        pub customer: TableRow<Customer>,
        pub invoice_date: Date,
        pub billing_address: Option<String>,
        pub billing_city: Option<String>,
        pub billing_state: Option<String>,
        pub billing_country: Option<String>,
        pub billing_postal_code: Option<String>,
        pub total: f64,
    }
    pub struct InvoiceLine {
        #[version(..2)]
        pub invoice: TableRow<Invoice>,
        #[version(2..)]
        pub invoice_new: TableRow<Invoice>,
        pub track: TableRow<Track>,
        pub unit_price: f64,
        pub quantity: i64,
    }
    #[version(..2)]
    pub struct MediaType {
        pub name: String,
    }
    pub struct Playlist {
        pub name: String,
    }
    #[unique(playlist, track)]
    pub struct PlaylistTrack {
        pub playlist: TableRow<Playlist>,
        #[index]
        pub track: TableRow<Track>,
    }
    pub struct Track {
        pub name: String,
        #[index]
        pub album: TableRow<Album>,
        #[version(..2)]
        pub media_type: TableRow<MediaType>,
        #[version(2..)]
        pub media_type: String,
        #[index]
        pub genre: TableRow<Genre>,
        pub composer: Option<String>,
        #[version(2..)]
        pub composer_table: Option<TableRow<Composer>>,
        pub milliseconds: i64,
        pub bytes: i64,
        pub unit_price: f64,
        #[version(2..)]
        pub byte_price: f64,
        #[version(1..)]
        pub favorite: bool,
    }
    #[version(2..)]
    pub struct Composer {
        pub name: String,
    }
}

pub fn migrate() -> Database<v2::Schema> {
    if !fs::exists("Chinook_Sqlite.sqlite").unwrap() {
        panic!(
            "test data file 'Chinook_Sqlite.sqlite' does not exist.
            Please download it from https://github.com/lerocha/chinook-database/releases/tag/v1.4.5"
        );
    }
    let config = Config::open_in_memory();

    let genre_extra = HashMap::from([("rock", 10)]);
    let m = Database::migrator(config).unwrap();
    let m = m
        .fixup(|txn| {
            txn.downgrade().rusqlite_transaction(|txn| {
                txn.execute_batch("ATTACH 'Chinook_Sqlite.sqlite' AS old;")
                    .unwrap();
                txn.execute_batch(include_str!("migrate.sql")).unwrap();
            })
        })
        .migrate(|txn| v0::migrate::Schema {
            genre_new: txn.migrate_ok(|old: Lazy<v0::Genre>| v0::migrate::GenreNew {
                name: old.name.clone(),
            }),
            short_genre: {
                let Ok(()) = txn.migrate_optional(|old: Lazy<v0::Genre>| {
                    (old.name.len() <= 10).then_some(v0::migrate::GenreNew {
                        name: old.name.clone(),
                    })
                });
                Migrated::map_fk_err(|| panic!())
            },
            track: txn.migrate_ok(|_old| v0::migrate::Track { favorite: false }),
        });

    let m = m.migrate(|txn| v1::migrate::Schema {
        customer: txn.migrate_ok(|old: Lazy<v1::Customer>| {
            v1::migrate::Customer {
                // lets do some cursed phone number parsing :D
                phone: old.phone.as_ref().and_then(|x| x.parse().ok()),
            }
        }),
        track: txn.migrate_ok(|old: Lazy<v1::Track>| v1::migrate::Track {
            media_type: old.media_type.name.clone(),
            composer_table: None,
            byte_price: old.unit_price / old.bytes as f64,
        }),
        genre_new: txn.migrate_ok(|old: Lazy<v1::GenreNew>| v1::migrate::GenreNew {
            extra: genre_extra.get(&*old.name).copied().unwrap_or(0),
        }),
        employee: txn.migrate_ok(|_| v1::migrate::Employee {}),
        invoice_line: txn.migrate_ok(|old: Lazy<v1::InvoiceLine>| v1::migrate::InvoiceLine {
            invoice_new: old.invoice.table_row(),
        }),
    });

    m.finish().unwrap()
}

#[cfg(test)]
#[cfg(feature = "dev")]
mod tests {
    use expect_test::expect;

    use super::*;

    #[test]
    fn backwards_compat() {
        use rust_query::migration::hash_schema;

        expect!["64bf0828bbfdf867"].assert_eq(&hash_schema::<v0::Schema>());
        expect!["3449b981d786c043"].assert_eq(&hash_schema::<v1::Schema>());
    }
}