diff --git a/.env b/.env new file mode 100644 index 0000000..be42785 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +DATABASE_URL="sqlite://test-db/dicebot.sqlite" +SQLX_OFFLINE="true" diff --git a/.gitignore b/.gitignore index 6c5b343..5030d40 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ bot-db* # We store a disabled async test in this file bigboy .#* +*.sqlite diff --git a/Cargo.lock b/Cargo.lock index ff8523e..7e0aea0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,23 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "ahash" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" + +[[package]] +name = "ahash" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796540673305a66d127804eef19ad696f1f204b8c1025aaca4958c17eab32877" +dependencies = [ + "getrandom 0.2.2", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -98,6 +115,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atoi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.5.0" @@ -138,6 +164,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "barrel" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d67c978b1322c8031145b1f6c236fc371292f52c565bc96018b2971afcbffe1" + [[package]] name = "base-x" version = "0.2.8" @@ -165,6 +197,18 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bitvec" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -174,6 +218,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "build_const" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7" + [[package]] name = "bumpalo" version = "3.6.1" @@ -227,6 +277,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time 0.1.43", + "winapi", +] + [[package]] name = "cipher" version = "0.2.5" @@ -292,6 +355,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +dependencies = [ + "build_const", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -301,6 +373,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.4" @@ -314,6 +396,16 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.4" @@ -389,11 +481,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +dependencies = [ + "serde", +] [[package]] name = "encoding_rs" @@ -417,6 +518,18 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fnv" version = "1.0.7" @@ -458,6 +571,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "futf" version = "0.1.4" @@ -671,6 +790,27 @@ name = "hashbrown" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash 0.4.7", +] + +[[package]] +name = "hashlink" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -681,6 +821,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.10.1" @@ -901,6 +1047,17 @@ version = "0.2.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" +[[package]] +name = "libsqlite3-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d31059f22935e6c31830db5249ba2b7ecd54fd73a9909286f0a67aa55c2fbd" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.4" @@ -1136,6 +1293,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "ntapi" version = "0.3.6" @@ -1145,6 +1315,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -1433,6 +1622,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "rand" version = "0.7.3" @@ -1543,6 +1738,50 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "refinery" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e29bd9c881127d714f4b5b9fdd9ea7651f3dd254922e959a10f6ada620e841da" +dependencies = [ + "refinery-core", + "refinery-macros", +] + +[[package]] +name = "refinery-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53260bc01535ea10c553ce0fc410609ba2dc0a9f4c9b4503e0af842dd4a6f89d" +dependencies = [ + "async-trait", + "cfg-if", + "chrono", + "lazy_static", + "log", + "regex", + "rusqlite", + "serde", + "siphasher", + "thiserror", + "toml", + "url", + "walkdir", +] + +[[package]] +name = "refinery-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79ff62c9b674b62c06a09cc8becf06cbafba9952afa1d8174e7e15f2c4ed43" +dependencies = [ + "proc-macro2", + "quote", + "refinery-core", + "regex", + "syn", +] + [[package]] name = "regex" version = "1.5.4" @@ -1775,6 +2014,21 @@ dependencies = [ "syn", ] +[[package]] +name = "rusqlite" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38ee71cbab2c827ec0ac24e76f82eca723cee92c509a65f67dee393c25112" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + [[package]] name = "rustc_version" version = "0.2.3" @@ -1790,6 +2044,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.19" @@ -1876,6 +2139,7 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ + "indexmap", "itoa", "ryu", "serde", @@ -1965,6 +2229,105 @@ dependencies = [ "winapi", ] +[[package]] +name = "sqlformat" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c" +dependencies = [ + "lazy_static", + "maplit", + "nom 6.1.2", + "regex", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2739d54a2ae9fdd0f545cb4e4b5574efb95e2ec71b7f921678e246fb20dcaaf" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1cad9cae4ca8947eba1a90e8ec7d3c59e7a768e2f120dc9013b669c34a90711" +dependencies = [ + "ahash 0.6.3", + "atoi", + "bitflags", + "byteorder", + "bytes", + "crc", + "crossbeam-channel", + "crossbeam-queue", + "crossbeam-utils", + "either", + "futures-channel", + "futures-core", + "futures-util", + "hashlink", + "hex", + "itoa", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "once_cell", + "parking_lot", + "percent-encoding", + "serde", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "tokio-stream", + "url", + "whoami", +] + +[[package]] +name = "sqlx-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01caee2b3935b4efe152f3262afbe51546ce3b1fc27ad61014e1b3cf5f55366e" +dependencies = [ + "dotenv", + "either", + "futures", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce2e16b6774c671cc183e1d202386fdf9cde1e8468c1894a7f2a63eb671c4f4" +dependencies = [ + "native-tls", + "once_cell", + "tokio", + "tokio-native-tls", +] + [[package]] name = "standback" version = "0.2.17" @@ -2054,6 +2417,16 @@ dependencies = [ "quote", ] +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "subtle" version = "2.4.0" @@ -2083,6 +2456,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.2.0" @@ -2113,6 +2492,7 @@ name = "tenebrous-dicebot" version = "0.10.0" dependencies = [ "async-trait", + "barrel", "bincode", "byteorder", "combine", @@ -2125,11 +2505,13 @@ dependencies = [ "log", "matrix-sdk", "memmem", - "nom", + "nom 5.1.2", "phf", "rand 0.8.3", + "refinery", "serde", "sled", + "sqlx", "thiserror", "tokio", "toml", @@ -2270,6 +2652,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.6.7" @@ -2371,6 +2764,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + [[package]] name = "unicode-width" version = "0.1.8" @@ -2383,6 +2782,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unindent" version = "0.1.7" @@ -2439,6 +2844,17 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" @@ -2539,6 +2955,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "whoami" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wildmatch" version = "2.1.0" @@ -2585,6 +3011,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "xml5ever" version = "0.16.1" diff --git a/Cargo.toml b/Cargo.toml index ec26237..acb3a22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,10 @@ repository = 'https://git.agnos.is/projectmoon/matrix-dicebot' keywords = ["games", "dice", "matrix", "bot"] categories = ["games"] +[[bin]] +name = "dicebot-migrate" +path = "src/migrate_cli.rs" + [dependencies] log = "0.4" env_logger = "0.8" @@ -32,6 +36,12 @@ bincode = "1.3" html2text = "0.2" phf = { version = "0.8", features = ["macros"] } matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "master" } +refinery = { version = "0.5", features = ["rusqlite"]} +barrel = { version = "0.6", features = ["sqlite3"] } + +[dependencies.sqlx] +version = "0.5" +features = [ "offline", "sqlite", "runtime-tokio-native-tls" ] [dependencies.serde] version = "1" diff --git a/sqlx-data.json b/sqlx-data.json new file mode 100644 index 0000000..9d050f1 --- /dev/null +++ b/sqlx-data.json @@ -0,0 +1,45 @@ +{ + "db": "SQLite", + "636b1b868eaf04cd234fbf17747d94a66e81f7bc1b060ba14151dbfaf40eeefc": { + "query": "SELECT value as \"value: i32\" FROM user_variables\n WHERE user_id = ? AND room_id = ? AND key = ?", + "describe": { + "columns": [ + { + "name": "value: i32", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false + ] + } + }, + "d6558668b7395b95ded8da71c80963ddde957abdcc3c68b03431f8e904e0d21f": { + "query": "SELECT key, value as \"value: i32\" FROM user_variables\n WHERE room_id = ?", + "describe": { + "columns": [ + { + "name": "key", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "value: i32", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + } + } +} \ No newline at end of file diff --git a/src/bin/dicebot.rs b/src/bin/dicebot.rs index 1f4ff5c..8ed69ff 100644 --- a/src/bin/dicebot.rs +++ b/src/bin/dicebot.rs @@ -7,6 +7,7 @@ use tenebrous_dicebot::bot::DiceBot; use tenebrous_dicebot::config::*; use tenebrous_dicebot::db::Database; use tenebrous_dicebot::error::BotError; +use tenebrous_dicebot::migrator; use tenebrous_dicebot::state::DiceBotState; #[tokio::main] @@ -33,6 +34,9 @@ async fn run() -> Result<(), BotError> { db.migrate(cfg.migration_version())?; + let sqlite_path = format!("{}/dicebot.sqlite", cfg.database_path()); + migrator::migrate(&sqlite_path).await?; + match DiceBot::new(&cfg, &state, &db) { Ok(bot) => bot.run().await?, Err(e) => println!("Error connecting: {:?}", e), diff --git a/src/db.rs b/src/db.rs index b50e92a..4036e3c 100644 --- a/src/db.rs +++ b/src/db.rs @@ -12,6 +12,7 @@ pub mod errors; pub mod migrations; pub mod rooms; pub mod schema; +pub mod sqlite; pub mod state; pub mod variables; diff --git a/src/db/sqlite/errors.rs b/src/db/sqlite/errors.rs new file mode 100644 index 0000000..0078b4e --- /dev/null +++ b/src/db/sqlite/errors.rs @@ -0,0 +1,81 @@ +use sled::transaction::{TransactionError, UnabortableTransactionError}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum MigrationError { + #[error("cannot downgrade to an older database version")] + CannotDowngrade, + + #[error("migration for version {0} not defined")] + MigrationNotFound(u32), + + #[error("migration failed: {0}")] + MigrationFailed(String), +} + +//TODO better combining of key and value in certain errors (namely +//I32SchemaViolation). +#[derive(Error, Debug)] +pub enum DataError { + #[error("value does not exist for key: {0}")] + KeyDoesNotExist(String), + + #[error("too many entries")] + TooManyEntries, + + #[error("expected i32, but i32 schema was violated")] + I32SchemaViolation, + + #[error("unexpected or corruptd data bytes")] + InvalidValue, + + #[error("expected string ref, but utf8 schema was violated: {0}")] + Utf8RefSchemaViolation(#[from] std::str::Utf8Error), + + #[error("expected string, but utf8 schema was violated: {0}")] + Utf8SchemaViolation(#[from] std::string::FromUtf8Error), + + #[error("internal database error: {0}")] + InternalError(#[from] sled::Error), + + #[error("transaction error: {0}")] + TransactionError(#[from] sled::transaction::TransactionError), + + #[error("unabortable transaction error: {0}")] + UnabortableTransactionError(#[from] UnabortableTransactionError), + + #[error("data migration error: {0}")] + MigrationError(#[from] MigrationError), + + #[error("deserialization error: {0}")] + DeserializationError(#[from] bincode::Error), + + #[error("sqlx error: {0}")] + SqlxError(#[from] sqlx::Error), +} + +/// This From implementation is necessary to deal with the recursive +/// error type in the error enum. We defined a transaction error, but +/// the only place we use it is when converting from +/// sled::transaction::TransactionError. This converter +/// extracts the inner data error from transaction aborted errors, and +/// forwards anything else onward as-is, but wrapped in DataError. +impl From> for DataError { + fn from(error: TransactionError) -> Self { + match error { + TransactionError::Abort(data_err) => data_err, + TransactionError::Storage(storage_err) => { + DataError::TransactionError(TransactionError::Storage(storage_err)) + } + } + } +} + +/// Automatically aborts transactions that hit a DataError by using +/// the try (question mark) operator when this trait implementation is +/// in scope. +impl From for sled::transaction::ConflictableTransactionError { + fn from(error: DataError) -> Self { + sled::transaction::ConflictableTransactionError::Abort(error) + } +} diff --git a/src/db/sqlite/mod.rs b/src/db/sqlite/mod.rs new file mode 100644 index 0000000..ac49130 --- /dev/null +++ b/src/db/sqlite/mod.rs @@ -0,0 +1,80 @@ +use async_trait::async_trait; +use errors::DataError; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; +use sqlx::ConnectOptions; +use sqlx::Connection; +use std::collections::HashMap; +use std::path::Path; +use std::str::FromStr; + +pub mod errors; +pub mod variables; + +// TODO move this up to the top once we delete sled. Traits will be the +// main API, then we can have different impls for different DBs. +#[async_trait] +pub(crate) trait Variables { + async fn get_user_variables( + &self, + user: &str, + room_id: &str, + ) -> Result, DataError>; + + async fn get_variable_count(&self, user: &str, room_id: &str) -> Result; + + async fn get_user_variable( + &self, + user: &str, + room_id: &str, + variable_name: &str, + ) -> Result; + + async fn set_user_variable( + &self, + user: &str, + room_id: &str, + variable_name: &str, + value: i32, + ) -> Result<(), DataError>; + + async fn delete_user_variable( + &self, + user: &str, + room_id: &str, + variable_name: &str, + ) -> Result<(), DataError>; +} + +pub struct Database { + conn: SqlitePool, +} + +impl Database { + fn new_db(conn: SqlitePool) -> Result { + let database = Database { conn: conn.clone() }; + + Ok(database) + } + + pub async fn new(path: &str) -> Result { + //Create database if missing. + let conn = SqliteConnectOptions::from_str(path)? + .create_if_missing(true) + .connect() + .await?; + + drop(conn); + + //Return actual conncetion pool. + let conn = SqlitePoolOptions::new() + .max_connections(5) + .connect(path) + .await?; + + Self::new_db(conn) + } + + pub async fn new_temp() -> Result { + Self::new("sqlite::memory:").await + } +} diff --git a/src/db/sqlite/variables.rs b/src/db/sqlite/variables.rs new file mode 100644 index 0000000..6e30edd --- /dev/null +++ b/src/db/sqlite/variables.rs @@ -0,0 +1,92 @@ +use super::errors::DataError; +use super::{Database, Variables}; +use async_trait::async_trait; +use std::collections::HashMap; + +struct UserVariableRow { + key: String, + value: i32, +} + +#[async_trait] +impl Variables for Database { + async fn get_user_variables( + &self, + user: &str, + room_id: &str, + ) -> Result, DataError> { + let rows = sqlx::query!( + r#"SELECT key, value as "value: i32" FROM user_variables + WHERE room_id = ?"#, + room_id, + ) + .fetch_all(&self.conn) + .await?; + + Ok(rows.into_iter().map(|row| (row.key, row.value)).collect()) + } + + async fn get_variable_count(&self, user: &str, room_id: &str) -> Result { + Ok(1) + } + + async fn get_user_variable( + &self, + user: &str, + room_id: &str, + variable_name: &str, + ) -> Result { + let row = sqlx::query!( + r#"SELECT value as "value: i32" FROM user_variables + WHERE user_id = ? AND room_id = ? AND key = ?"#, + user, + room_id, + variable_name + ) + .fetch_one(&self.conn) + .await?; + + Ok(row.value) + } + + async fn set_user_variable( + &self, + user: &str, + room_id: &str, + variable_name: &str, + value: i32, + ) -> Result<(), DataError> { + sqlx::query( + "INSERT INTO user_variables + (user_id, room_id, variable_name, value) + values (?, ?, ?, ?)", + ) + .bind(user) + .bind(room_id) + .bind(variable_name) + .bind(value) + .execute(&self.conn) + .await?; + + Ok(()) + } + + async fn delete_user_variable( + &self, + user: &str, + room_id: &str, + variable_name: &str, + ) -> Result<(), DataError> { + sqlx::query( + "DELETE FROM user_variables + WHERE user_id = ? AND room_id = ? AND variable_name = ?", + ) + .bind(user) + .bind(room_id) + .bind(variable_name) + .execute(&self.conn) + .await?; + + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs index 69b2004..ca7bd93 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ -use crate::commands::CommandError; use crate::config::ConfigError; use crate::db::errors::DataError; +use crate::{commands::CommandError, migrator::migrations}; use thiserror::Error; #[derive(Error, Debug)] @@ -73,6 +73,9 @@ pub enum BotError { #[error("database error")] DatabaseError(#[from] sled::Error), + #[error("database migration error: {0}")] + SqliteError(#[from] crate::migrator::MigrationError), + #[error("too many commands or message was too large")] MessageTooLarge, diff --git a/src/lib.rs b/src/lib.rs index d67ade8..5c27f63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod error; mod help; pub mod logic; pub mod matrix; +pub mod migrator; pub mod models; mod parser; pub mod state; diff --git a/src/migrate_cli.rs b/src/migrate_cli.rs new file mode 100644 index 0000000..11cbbbc --- /dev/null +++ b/src/migrate_cli.rs @@ -0,0 +1,17 @@ +use std::env; + +pub mod migrator; + +#[tokio::main] +async fn main() -> Result<(), migrator::MigrationError> { + let args: Vec = env::args().collect(); + let db_path: &str = match &args[..] { + [_, path] => path.as_ref(), + [_, _, ..] => panic!("Expected exactly 0 or 1 argument"), + _ => "dicebot.sqlite", + }; + + println!("Using database: {}", db_path); + + migrator::migrate(db_path).await +} diff --git a/src/migrator.rs b/src/migrator.rs new file mode 100644 index 0000000..33be059 --- /dev/null +++ b/src/migrator.rs @@ -0,0 +1,33 @@ +use log::info; +use refinery::config::{Config, ConfigDbType}; +use sqlx::sqlite::SqliteConnectOptions; +use sqlx::ConnectOptions; +use std::str::FromStr; +use thiserror::Error; + +pub mod migrations; + +#[derive(Error, Debug)] +pub enum MigrationError { + #[error("sqlite connection error: {0}")] + SqlxError(#[from] sqlx::Error), + + #[error("refinery migration error: {0}")] + RefineryError(#[from] refinery::Error), +} + +/// Run database migrations against the sqlite database. +pub async fn migrate(db_path: &str) -> Result<(), MigrationError> { + //Create database if missing. + let conn = SqliteConnectOptions::from_str(&format!("sqlite://{}", db_path))? + .create_if_missing(true) + .connect() + .await?; + + drop(conn); + + let mut conn = Config::new(ConfigDbType::Sqlite).set_db_path(db_path); + info!("Running migrations"); + migrations::runner().run(&mut conn)?; + Ok(()) +} diff --git a/src/migrator/migrations/V1__variables.rs b/src/migrator/migrations/V1__variables.rs new file mode 100644 index 0000000..298233a --- /dev/null +++ b/src/migrator/migrations/V1__variables.rs @@ -0,0 +1,18 @@ +use barrel::backend::Sqlite; +use barrel::{types, Migration}; +use log::info; + +pub fn migration() -> String { + let mut m = Migration::new(); + info!("Applying migration: {}", file!()); + + m.create_table("user_variables", |t| { + t.add_column("id", types::primary()); + t.add_column("room_id", types::text()); + t.add_column("user_id", types::text()); + t.add_column("key", types::text()); + t.add_column("value", types::integer()); + }); + + m.make::() +} diff --git a/src/migrator/migrations/V2__rooms.rs b/src/migrator/migrations/V2__rooms.rs new file mode 100644 index 0000000..14f463b --- /dev/null +++ b/src/migrator/migrations/V2__rooms.rs @@ -0,0 +1,31 @@ +use barrel::backend::Sqlite; +use barrel::{types, Migration}; +use log::info; + +pub fn migration() -> String { + let mut m = Migration::new(); + info!("Applying migration: {}", file!()); + + //Table for basic room information: room ID, room name + m.create_table("room_info", move |t| { + t.add_column("id", types::primary()); + t.add_column("room_id", types::text()); + t.add_column("room_name", types::text()); + }); + + //Table of users in rooms. + m.create_table("room_users", move |t| { + t.add_column("id", types::primary()); + t.add_column("room_id", types::text()); + t.add_column("username", types::text()); + }); + + //Table of room ID, event ID, event timestamp + m.create_table("room_events", move |t| { + t.add_column("id", types::primary()); + t.add_column("room_id", types::text()); + t.add_column("event_id", types::text()); + t.add_column("event_timestamp", types::integer()); + }); + m.make::() +} diff --git a/src/migrator/migrations/mod.rs b/src/migrator/migrations/mod.rs new file mode 100644 index 0000000..b237433 --- /dev/null +++ b/src/migrator/migrations/mod.rs @@ -0,0 +1,2 @@ +use refinery::include_migration_mods; +include_migration_mods!("src/migrator/migrations");