diff --git a/.gitignore b/.gitignore index 885013f..b836868 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ shimmering/assets/b30_background.* target backups dump.sql +schema.sql diff --git a/Cargo.lock b/Cargo.lock index 0c7b8b2..19e477b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,12 +60,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -133,15 +127,6 @@ dependencies = [ "syn 2.0.66", ] -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - [[package]] name = "autocfg" version = "1.3.0" @@ -198,12 +183,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - [[package]] name = "bit_field" version = "0.10.2" @@ -218,12 +197,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -dependencies = [ - "serde", -] +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitstream-io" @@ -315,13 +291,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.99" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -361,21 +337,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "core-foundation" version = "0.9.4" @@ -437,21 +398,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.4.2" @@ -489,15 +435,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -585,17 +522,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" -[[package]] -name = "der" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - [[package]] name = "deranged" version = "0.3.11" @@ -624,9 +550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", - "subtle", ] [[package]] @@ -659,12 +583,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "dwrote" version = "0.11.0" @@ -682,9 +600,6 @@ name = "either" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" -dependencies = [ - "serde", -] [[package]] name = "encoding_rs" @@ -720,28 +635,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - -[[package]] -name = "event-listener" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - [[package]] name = "exr" version = "1.72.0" @@ -758,6 +651,18 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.1.0" @@ -795,9 +700,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ - "futures-core", - "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -812,7 +715,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2845a73bbd781e691ab7c2a028c579727cd254942e8ced57ff73e0eafd60de87" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "core-foundation", "core-graphics", @@ -873,7 +776,7 @@ version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5442dee36ca09604133580dc0553780e867936bb3cbef3275859e889026d2b17" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "freetype-sys", "libc", ] @@ -919,28 +822,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" -[[package]] -name = "futures-executor" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - [[package]] name = "futures-io" version = "0.3.30" @@ -1088,7 +969,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", - "allocator-api2", ] [[package]] @@ -1112,39 +992,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "http" version = "0.2.12" @@ -1348,6 +1195,25 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -1419,9 +1285,6 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -dependencies = [ - "spin 0.5.2", -] [[package]] name = "lebe" @@ -1468,15 +1331,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", ] [[package]] name = "libsqlite3-sys" -version = "0.28.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -1534,16 +1397,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - [[package]] name = "memchr" version = "2.7.4" @@ -1669,23 +1522,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - [[package]] name = "num-complex" version = "0.4.6" @@ -1799,12 +1635,6 @@ dependencies = [ "ttf-parser 0.24.1", ] -[[package]] -name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - [[package]] name = "parking_lot" version = "0.12.3" @@ -1823,7 +1653,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.2", + "redox_syscall", "smallvec", "windows-targets 0.52.5", ] @@ -1853,15 +1683,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -1880,27 +1701,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - [[package]] name = "pkg-config" version = "0.3.30" @@ -2037,7 +1837,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "memchr", "unicase", ] @@ -2066,6 +1866,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_sqlite" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb14dba8247a6a15b7fdbc7d389e2e6f03ee9f184f87117706d509c092dfe846" +dependencies = [ + "r2d2", + "rusqlite", + "uuid", +] + [[package]] name = "rand" version = "0.8.5" @@ -2182,22 +2004,13 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -2303,29 +2116,35 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] -name = "rsa" -version = "0.9.6" +name = "rusqlite" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "signature", - "spki", - "subtle", - "zeroize", + "bitflags 2.6.0", + "chrono", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rusqlite_migration" +version = "1.3.0-alpha-without-tokio.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7756e1e9f1fb9574c499e66d9e3272e0440fc0ed109f425036e2a6e95e2ad818" +dependencies = [ + "include_dir", + "log", + "rusqlite", ] [[package]] @@ -2349,7 +2168,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -2442,6 +2261,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2547,7 +2375,7 @@ dependencies = [ "arrayvec", "async-trait", "base64 0.22.1", - "bitflags 2.5.0", + "bitflags 2.6.0", "bytes", "chrono", "dashmap", @@ -2582,17 +2410,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shimmeringmoon" version = "0.1.0" @@ -2602,22 +2419,22 @@ dependencies = [ "hypertesseract", "image 0.25.2", "imageproc", + "include_dir", "num", "plotters", "poise", - "sqlx", + "r2d2", + "r2d2_sqlite", + "rusqlite", + "rusqlite_migration", "tokio", ] [[package]] -name = "signature" -version = "2.2.0" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simba" @@ -2676,9 +2493,6 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -dependencies = [ - "serde", -] [[package]] name = "socket2" @@ -2690,12 +2504,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -2705,235 +2513,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlformat" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" -dependencies = [ - "nom", - "unicode_categories", -] - -[[package]] -name = "sqlx" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27144619c6e5802f1380337a209d2ac1c431002dd74c6e60aebff3c506dc4f0c" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a999083c1af5b5d6c071d34a708a19ba3e02106ad82ef7bbd69f5e48266b613b" -dependencies = [ - "atoi", - "byteorder", - "bytes", - "chrono", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-channel", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown", - "hashlink", - "hex", - "indexmap", - "log", - "memchr", - "once_cell", - "paste", - "percent-encoding", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlformat", - "thiserror", - "tokio", - "tokio-stream", - "tracing", - "url", -] - -[[package]] -name = "sqlx-macros" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23217eb7d86c584b8cbe0337b9eacf12ab76fe7673c513141ec42565698bb88" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 2.0.66", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a099220ae541c5db479c6424bdf1b200987934033c2584f79a0e1693601e776" -dependencies = [ - "dotenvy", - "either", - "heck", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn 2.0.66", - "tempfile", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5afe4c38a9b417b6a9a5eeffe7235d0a106716495536e7727d1c7f4b1ff3eba6" -dependencies = [ - "atoi", - "base64 0.22.1", - "bitflags 2.5.0", - "byteorder", - "bytes", - "chrono", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand", - "rsa", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1dbb157e65f10dbe01f729339c06d239120221c9ad9fa0ba8408c4cc18ecf21" -dependencies = [ - "atoi", - "base64 0.22.1", - "bitflags 2.5.0", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "rand", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2cdd83c008a622d94499c0006d8ee5f821f36c89b7d625c900e5dc30b5c5ee" -dependencies = [ - "atoi", - "chrono", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "serde_urlencoded", - "sqlx-core", - "tracing", - "url", -] - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "strsim" version = "0.11.1" @@ -3176,17 +2755,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-tungstenite" version = "0.21.0" @@ -3404,18 +2972,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-properties" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" - -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "untrusted" version = "0.9.0" @@ -3440,6 +2996,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", + "rand", +] + [[package]] name = "v_frame" version = "0.3.8" @@ -3494,12 +3060,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -3610,16 +3170,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" -[[package]] -name = "whoami" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" -dependencies = [ - "redox_syscall 0.4.1", - "wasite", -] - [[package]] name = "wide" version = "0.7.26" diff --git a/Cargo.toml b/Cargo.toml index 61c9553..f68af09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,14 @@ image = "0.25.2" num = "0.4.3" plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c", features=["bitmap_backend"] } poise = "0.6.1" -sqlx = { version = "0.8.0", features = ["sqlite", "runtime-tokio", "chrono"] } hypertesseract = { features=["image"], git="https://github.com/BlueGhostGH/hypertesseract.git", rev="4e05063" } tokio = {version="1.38.0", features=["rt-multi-thread"]} imageproc = "0.25.0" +rusqlite = { version = "0.32.1", features = ["bundled", "chrono"] } +r2d2_sqlite = "0.25.0" +r2d2 = "0.8.10" +rusqlite_migration = {version="1.3.0-alpha-without-tokio.1", features = ["from-directory"]} +include_dir = "0.7.4" [profile.dev.package."*"] opt-level = 3 diff --git a/migrations/01-create-users-table/up.sql b/migrations/01-create-users-table/up.sql new file mode 100644 index 0000000..6c2fab9 --- /dev/null +++ b/migrations/01-create-users-table/up.sql @@ -0,0 +1,36 @@ +-- {{{ users +create table IF NOT EXISTS users ( + id INTEGER NOT NULL PRIMARY KEY, + discord_id TEXT UNIQUE NOT NULL, + is_pookie BOOL NOT NULL DEFAULT 0 +); +-- }}} +-- {{{ plays +CREATE TABLE IF NOT EXISTS plays ( + id INTEGER NOT NULL PRIMARY KEY, + chart_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + discord_attachment_id TEXT, + + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + + max_recall INTEGER, + far_notes INTEGER, + + FOREIGN KEY (chart_id) REFERENCES charts(id), + FOREIGN KEY (user_id) REFERENCES users(id) +); +-- }}} +-- {{{ scores +CREATE TABLE IF NOT EXISTS scores ( + id INTEGER NOT NULL PRIMARY KEY, + play_id INTEGER NOT NULL, + + score INTEGER NOT NULL, + creation_ptt INTEGER, + scoring_system TEXT NOT NULL CHECK (scoring_system IN ('standard', 'sdf', 'ex')), + + FOREIGN KEY (play_id) REFERENCES plays(id), + UNIQUE(play_id, scoring_system) +) +-- }}} diff --git a/migrations/02-create-charts-and-songs/up.sql b/migrations/02-create-charts-and-songs/up.sql new file mode 100644 index 0000000..1f9fa52 --- /dev/null +++ b/migrations/02-create-charts-and-songs/up.sql @@ -0,0 +1,29 @@ +-- {{{ songs +CREATE TABLE IF NOT EXISTS songs ( + id INTEGER NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + artist TEXT NOT NULL, + side TEXT NOT NULL CHECK (side IN ('light', 'conflict', 'silent')), + bpm TEXT NOT NULL, + pack TEXT, + + UNIQUE(title, artist) +); +-- }}} +-- {{{ charts +CREATE TABLE IF NOT EXISTS charts ( + id INTEGER NOT NULL PRIMARY KEY, + song_id INTEGER NOT NULL, + note_design TEXT, + shorthand TEXT, + + difficulty TEXT NOT NULL CHECK (difficulty IN ('PST','PRS','FTR','ETR','BYD')), + level TEXT NOT NULL, + + note_count INTEGER NOT NULL, + chart_constant INTEGER NOT NULL, + + FOREIGN KEY (song_id) REFERENCES songs(id), + UNIQUE(song_id, difficulty) +); +-- }}} diff --git a/migrations/03-create-plays-and-scores/up.sql b/migrations/03-create-plays-and-scores/up.sql new file mode 100644 index 0000000..1c37f09 --- /dev/null +++ b/migrations/03-create-plays-and-scores/up.sql @@ -0,0 +1,29 @@ +-- {{{ plays +CREATE TABLE IF NOT EXISTS plays ( + id INTEGER NOT NULL PRIMARY KEY, + chart_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + discord_attachment_id TEXT, + + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + + max_recall INTEGER, + far_notes INTEGER, + + FOREIGN KEY (chart_id) REFERENCES charts(id), + FOREIGN KEY (user_id) REFERENCES users(id) +); +-- }}} +-- {{{ scores +CREATE TABLE IF NOT EXISTS scores ( + id INTEGER NOT NULL PRIMARY KEY, + play_id INTEGER NOT NULL, + + score INTEGER NOT NULL, + creation_ptt INTEGER, + scoring_system TEXT NOT NULL CHECK (scoring_system IN ('standard', 'sdf', 'ex')), + + FOREIGN KEY (play_id) REFERENCES plays(id), + UNIQUE(play_id, scoring_system) +) +-- }}} diff --git a/schema.sql b/schema.sql deleted file mode 100644 index bf0c9dc..0000000 --- a/schema.sql +++ /dev/null @@ -1,67 +0,0 @@ -# {{{ users -create table IF NOT EXISTS users ( - id INTEGER NOT NULL PRIMARY KEY, - discord_id TEXT UNIQUE NOT NULL, - is_pookie BOOL NOT NULL DEFAULT 0 -); -# }}} -# {{{ songs -CREATE TABLE IF NOT EXISTS songs ( - id INTEGER NOT NULL PRIMARY KEY, - title TEXT NOT NULL, - artist TEXT NOT NULL, - side TEXT NOT NULL CHECK (side IN ('light', 'conflict', 'silent')), - bpm TEXT NOT NULL, - pack TEXT, - - UNIQUE(title, artist) -); -# }}} -# {{{ charts -CREATE TABLE IF NOT EXISTS charts ( - id INTEGER NOT NULL PRIMARY KEY, - song_id INTEGER NOT NULL, - note_design TEXT, - shorthand TEXT, - - difficulty TEXT NOT NULL CHECK (difficulty IN ('PST','PRS','FTR','ETR','BYD')), - level TEXT NOT NULL, - - note_count INTEGER NOT NULL, - chart_constant INTEGER NOT NULL, - - FOREIGN KEY (song_id) REFERENCES songs(id), - UNIQUE(song_id, difficulty) -); -# }}} -# {{{ plays -CREATE TABLE IF NOT EXISTS plays ( - id INTEGER NOT NULL PRIMARY KEY, - chart_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - discord_attachment_id TEXT, - - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - - max_recall INTEGER, - far_notes INTEGER, - - FOREIGN KEY (chart_id) REFERENCES charts(id), - FOREIGN KEY (user_id) REFERENCES users(id) -); -# }}} -# {{{ scores -CREATE TABLE IF NOT EXISTS scores ( - id INTEGER NOT NULL PRIMARY KEY, - play_id INTEGER NOT NULL, - - score INTEGER NOT NULL, - creation_ptt INTEGER, - scoring_system NOT NULL CHECK (scoring_system IN ('standard', 'sdf', 'ex')), - - FOREIGN KEY (play_id) REFERENCES plays(id), - UNIQUE(play_id, scoring_system) -) -# }}} - -insert into users(discord_id) values (385759924917108740); diff --git a/src/arcaea/achievement.rs b/src/arcaea/achievement.rs index 21bc90e..b9eac88 100644 --- a/src/arcaea/achievement.rs +++ b/src/arcaea/achievement.rs @@ -1,5 +1,4 @@ use image::RgbaImage; -use sqlx::query; use crate::{ assets::get_data_dir, @@ -122,16 +121,8 @@ impl GoalStats { user: &User, scoring_system: ScoringSystem, ) -> Result { - let plays = get_best_plays( - &ctx.db, - &ctx.song_cache, - user.id, - scoring_system, - 0, - usize::MAX, - None, - ) - .await??; + let plays = get_best_plays(ctx, user.id, scoring_system, 0, usize::MAX, None)??; + let conn = ctx.db.get()?; // {{{ PM count let pm_count = plays @@ -142,30 +133,31 @@ impl GoalStats { .count(); // }}} // {{{ Play count - let play_count = query!( - "SELECT count() as count FROM plays WHERE user_id=?", - user.id - ) - .fetch_one(&ctx.db) - .await? - .count as usize; + let play_count = conn + .prepare_cached("SELECT count() as count FROM plays WHERE user_id=?")? + .query_row([user.id], |row| row.get(0))?; // }}} // {{{ Peak ptt - let peak_ptt = query!( - " + let peak_ptt = conn + .prepare_cached( + " SELECT s.creation_ptt FROM plays p JOIN scores s ON s.play_id = p.id WHERE user_id = ? AND scoring_system = ? + ORDER BY s.creation_ptt DESC + LIMIT 1 ", - user.id, - ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()] - ) - .fetch_one(&ctx.db) - .await? - .creation_ptt - .ok_or_else(|| "No ptt history data found")? as u32; + )? + .query_row( + ( + user.id, + ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()], + ), + |row| row.get(0), + ) + .map_err(|_| "No ptt history data found")?; // }}} // {{{ Peak PM relay let peak_pm_relay = { diff --git a/src/arcaea/chart.rs b/src/arcaea/chart.rs index 0ca4a46..202f8fd 100644 --- a/src/arcaea/chart.rs +++ b/src/arcaea/chart.rs @@ -1,12 +1,15 @@ use std::{fmt::Display, num::NonZeroU16, path::PathBuf}; use image::{ImageBuffer, Rgb}; -use sqlx::SqlitePool; +use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef}; -use crate::{bitmap::Color, context::Error}; +use crate::{ + bitmap::Color, + context::{DbConnection, Error}, +}; // {{{ Difficuly -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Difficulty { PST, PRS, @@ -29,17 +32,19 @@ impl Difficulty { } } -impl TryFrom for Difficulty { - type Error = String; +impl FromSql for Difficulty { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + let str: String = rusqlite::types::FromSql::column_result(value)?; - fn try_from(value: String) -> Result { for (i, s) in Self::DIFFICULTY_SHORTHANDS.iter().enumerate() { - if value == **s { + if str == **s { return Ok(Self::DIFFICULTIES[i]); } } - Err(format!("Cannot convert {} to difficulty", value)) + FromSqlResult::Err(FromSqlError::Other( + format!("Cannot convert {} to difficulty", str).into(), + )) } } @@ -120,17 +125,19 @@ impl Display for Level { } } -impl TryFrom for Level { - type Error = String; +impl FromSql for Level { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + let str: String = rusqlite::types::FromSql::column_result(value)?; - fn try_from(value: String) -> Result { for (i, s) in Self::LEVEL_STRINGS.iter().enumerate() { - if value == **s { + if str == **s { return Ok(Self::LEVELS[i]); } } - Err(format!("Cannot convert {} to a level", value)) + FromSqlResult::Err(FromSqlError::Other( + format!("Cannot convert {} to level", str).into(), + )) } } // }}} @@ -152,17 +159,19 @@ impl Side { } } -impl TryFrom for Side { - type Error = String; +impl FromSql for Side { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + let str: String = rusqlite::types::FromSql::column_result(value)?; - fn try_from(value: String) -> Result { for (i, s) in Self::SIDE_STRINGS.iter().enumerate() { - if value == **s { + if str == **s { return Ok(Self::SIDES[i]); } } - Err(format!("Cannot convert {} to difficulty", value)) + FromSqlResult::Err(FromSqlError::Other( + format!("Cannot convert {} to side", str).into(), + )) } } // }}} @@ -304,44 +313,53 @@ impl SongCache { } // {{{ Populate cache - pub async fn new(pool: &SqlitePool) -> Result { + pub fn new(conn: &DbConnection) -> Result { + let conn = conn.get()?; let mut result = Self::default(); // {{{ Songs - let songs = sqlx::query!("SELECT * FROM songs").fetch_all(pool).await?; - for song in songs { - let song = Song { - id: song.id as u32, - lowercase_title: song.title.to_lowercase(), - title: song.title, - artist: song.artist, - pack: song.pack, - bpm: song.bpm, - side: Side::try_from(song.side)?, - }; + let mut query = conn.prepare_cached("SELECT * FROM songs")?; + let songs = query.query_map((), |row| { + Ok(Song { + id: row.get("id")?, + lowercase_title: row.get::<_, String>("title")?.to_lowercase(), + title: row.get("title")?, + artist: row.get("artist")?, + pack: row.get("pack")?, + bpm: row.get("bpm")?, + side: row.get("side")?, + }) + })?; + for song in songs { + let song = song?; let song_id = song.id as usize; if song_id >= result.songs.len() { result.songs.resize(song_id + 1, None); } + result.songs[song_id] = Some(CachedSong::new(song)); } // }}} // {{{ Charts - let charts = sqlx::query!("SELECT * FROM charts").fetch_all(pool).await?; - for chart in charts { - let chart = Chart { - id: chart.id as u32, - song_id: chart.song_id as u32, - shorthand: chart.shorthand, - difficulty: Difficulty::try_from(chart.difficulty)?, - level: Level::try_from(chart.level)?, - chart_constant: chart.chart_constant as u32, - note_count: chart.note_count as u32, + let mut query = conn.prepare_cached("SELECT * FROM charts")?; + let charts = query.query_map((), |row| { + Ok(Chart { + id: row.get("id")?, + song_id: row.get("song_id")?, + shorthand: row.get("shorthand")?, + difficulty: row.get("difficulty")?, + level: row.get("level")?, + chart_constant: row.get("chart_constant")?, + note_count: row.get("note_count")?, + note_design: row.get("note_design")?, cached_jacket: None, - note_design: chart.note_design, - }; + }) + })?; + + for chart in charts { + let chart = chart?; // {{{ Tie chart to song { diff --git a/src/arcaea/play.rs b/src/arcaea/play.rs index 9e59236..206e5ba 100644 --- a/src/arcaea/play.rs +++ b/src/arcaea/play.rs @@ -9,14 +9,12 @@ use num::Zero; use poise::serenity_prelude::{ Attachment, AttachmentId, CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp, }; -use sqlx::query_as; -use sqlx::{query, SqlitePool}; +use rusqlite::Row; use crate::arcaea::chart::{Chart, Song}; use crate::context::{Error, UserContext}; use crate::user::User; -use super::chart::SongCache; use super::rating::{rating_as_fixed, rating_as_float}; use super::score::{Score, ScoringSystem}; @@ -61,12 +59,14 @@ impl CreatePlay { } // {{{ Save - pub async fn save(self, ctx: &UserContext, user: &User, chart: &Chart) -> Result { + pub fn save(self, ctx: &UserContext, user: &User, chart: &Chart) -> Result { + let conn = ctx.db.get()?; let attachment_id = self.discord_attachment_id.map(|i| i.get() as i64); // {{{ Save current data to play - let play = sqlx::query!( - " + let (id, created_at) = conn + .prepare_cached( + " INSERT INTO plays( user_id,chart_id,discord_attachment_id, max_recall,far_notes @@ -74,88 +74,55 @@ impl CreatePlay { VALUES(?,?,?,?,?) RETURNING id, created_at ", - user.id, - chart.id, - attachment_id, - self.max_recall, - self.far_notes - ) - .fetch_one(&ctx.db) - .await?; + )? + .query_row( + ( + user.id, + chart.id, + attachment_id, + self.max_recall, + self.far_notes, + ), + |row| Ok((row.get("id")?, row.get("created_at")?)), + )?; // }}} // {{{ Update creation ptt data let scores = ScoreCollection::from_standard_score(self.score, chart); + for system in ScoringSystem::SCORING_SYSTEMS { let i = system.to_index(); - let plays = get_best_plays(&ctx.db, &ctx.song_cache, user.id, system, 30, 30, None) - .await? - .ok(); + let plays = get_best_plays(ctx, user.id, system, 30, 30, None)?.ok(); let creation_ptt: Option<_> = try { rating_as_fixed(compute_b30_ptt(system, &plays?)) }; - query!( + conn.prepare_cached( " INSERT INTO scores(play_id, score, creation_ptt, scoring_system) VALUES (?,?,?,?) ", - play.id, + )? + .execute(( + id, scores.0[i].0, creation_ptt, - ScoringSystem::SCORING_SYSTEM_DB_STRINGS[i] - ) - .execute(&ctx.db) - .await?; + ScoringSystem::SCORING_SYSTEM_DB_STRINGS[i], + ))?; } // }}} Ok(Play { - id: play.id as u32, - created_at: play.created_at, + id, + created_at, + scores, chart_id: chart.id, user_id: user.id, - scores, max_recall: self.max_recall, far_notes: self.far_notes, }) } // }}} } -// }}} -// {{{ DbPlay -/// Construct a `Play` from a sqlite return record. -#[macro_export] -macro_rules! play_from_db_record { - ($chart:expr, $record:expr) => {{ - use crate::arcaea::play::{Play, ScoreCollection}; - use crate::arcaea::score::Score; - Play { - id: $record.id as u32, - chart_id: $record.chart_id as u32, - user_id: $record.user_id as u32, - scores: ScoreCollection::from_standard_score(Score($record.score as u32), $chart), - max_recall: $record.max_recall.map(|r| r as u32), - far_notes: $record.far_notes.map(|r| r as u32), - created_at: $record.created_at, - } - }}; -} - -/// Typed version of the input to the macro above. -/// Useful when using the non-macro version of the sqlx functions. -#[derive(Debug, sqlx::FromRow)] -pub struct DbPlay { - pub id: i64, - pub chart_id: i64, - pub user_id: i64, - pub created_at: chrono::NaiveDateTime, - - // Score details - pub max_recall: Option, - pub far_notes: Option, - pub score: i64, -} - // }}} // {{{ Score data #[derive(Debug, Clone, Copy)] @@ -185,6 +152,20 @@ pub struct Play { } impl Play { + // {{{ Row parsing + #[inline] + pub fn from_sql(chart: &Chart, row: &Row) -> Result { + Ok(Play { + id: row.get("id")?, + chart_id: row.get("chart_id")?, + user_id: row.get("user_id")?, + created_at: row.get("created_at")?, + max_recall: row.get("max_recall")?, + far_notes: row.get("far_notes")?, + scores: ScoreCollection::from_standard_score(Score(row.get("score")?), chart), + }) + } + // }}} // {{{ Query the underlying score #[inline] pub fn score(&self, system: ScoringSystem) -> Score { @@ -272,9 +253,9 @@ impl Play { /// Creates a discord embed for this play. /// /// The `index` variable is only used to create distinct filenames. - pub async fn to_embed( + pub fn to_embed( &self, - db: &SqlitePool, + ctx: &UserContext, user: &User, song: &Song, chart: &Chart, @@ -282,33 +263,28 @@ impl Play { author: Option<&poise::serenity_prelude::User>, ) -> Result<(CreateEmbed, Option), Error> { // {{{ Get previously best score - let prev_play = query!( - " - SELECT - p.id, p.chart_id, p.user_id, p.created_at, - p.max_recall, p.far_notes, s.score - FROM plays p - JOIN scores s ON s.play_id = p.id - WHERE s.scoring_system='standard' - AND p.user_id=? - AND p.chart_id=? - AND p.created_at = Vec<(Play, &'a Song, &'a Chart)>; -pub async fn get_best_plays<'a>( - db: &SqlitePool, - song_cache: &'a SongCache, +pub fn get_best_plays<'a>( + ctx: &'a UserContext, user_id: u32, scoring_system: ScoringSystem, min_amount: usize, max_amount: usize, before: Option, ) -> Result, String>, Error> { + let conn = ctx.db.get()?; // {{{ DB data fetching - let plays: Vec = query_as( - " - SELECT - p.id, p.chart_id, p.user_id, p.created_at, - p.max_recall, p.far_notes, s.score, - MAX(s.score) as _cscore - -- ^ This is only here to make sqlite pick the correct row for the bare columns - FROM plays p - JOIN scores s ON s.play_id = p.id - JOIN scores cs ON cs.play_id = p.id - WHERE s.scoring_system='standard' - AND cs.scoring_system=? - AND p.user_id=? - AND p.created_at<=? - GROUP BY p.chart_id - ", - ) - .bind(ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()]) - .bind(user_id) - .bind(before.unwrap_or_else(|| Utc::now().naive_utc())) - .fetch_all(db) - .await?; + let mut plays = conn + .prepare_cached( + " + SELECT + p.id, p.chart_id, p.user_id, p.created_at, + p.max_recall, p.far_notes, s.score, + MAX(cs.score) as _cscore + -- ^ This is only here to make sqlite pick the correct row for the bare columns + FROM plays p + JOIN scores s ON s.play_id = p.id + JOIN scores cs ON cs.play_id = p.id + WHERE s.scoring_system='standard' + AND cs.scoring_system=? + AND p.user_id=? + AND p.created_at<=? + GROUP BY p.chart_id + ", + )? + .query_and_then( + ( + ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()], + user_id, + before.unwrap_or_else(|| Utc::now().naive_utc()), + ), + |row| { + let (song, chart) = ctx.song_cache.lookup_chart(row.get("chart_id")?)?; + let play = Play::from_sql(chart, row)?; + Ok((play, song, chart)) + }, + )? + .collect::, Error>>()?; // }}} if plays.len() < min_amount { @@ -450,17 +435,6 @@ pub async fn get_best_plays<'a>( } // {{{ B30 computation - // NOTE: we reallocate here, although we do not have much of a choice, - // unless we want to be lazy about things - let mut plays: Vec<(Play, &Song, &Chart)> = plays - .into_iter() - .map(|play| { - let (song, chart) = song_cache.lookup_chart(play.chart_id as u32)?; - let play = play_from_db_record!(chart, play); - Ok((play, song, chart)) - }) - .collect::, Error>>()?; - plays.sort_by_key(|(play, _, chart)| -play.play_rating(scoring_system, chart.chart_constant)); plays.truncate(max_amount); // }}} @@ -480,7 +454,8 @@ pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_> // }}} // {{{ Maintenance functions pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), Error> { - let plays = query!( + let conn = ctx.db.get()?; + let mut query = conn.prepare_cached( " SELECT p.id, p.chart_id, p.user_id, p.created_at, @@ -489,53 +464,44 @@ pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), Error> { JOIN scores s ON s.play_id = p.id WHERE s.scoring_system='standard' ORDER BY p.created_at ASC - " - ) - // Can't use the stream based version because of db locking... - .fetch_all(&ctx.db) - .await?; + ", + )?; + + let plays = query.query_and_then((), |row| -> Result<_, Error> { + let (_, chart) = ctx.song_cache.lookup_chart(row.get("chart_id")?)?; + let play = Play::from_sql(chart, row)?; + Ok(play) + })?; let mut i = 0; for play in plays { - let (_, chart) = ctx.song_cache.lookup_chart(play.chart_id as u32)?; - let play = play_from_db_record!(chart, play); - + let play = play?; for system in ScoringSystem::SCORING_SYSTEMS { let i = system.to_index(); - let plays = get_best_plays( - &ctx.db, - &ctx.song_cache, - play.user_id, - system, - 30, - 30, - Some(play.created_at), - ) - .await? - .ok(); + let plays = + get_best_plays(&ctx, play.user_id, system, 30, 30, Some(play.created_at))?.ok(); let creation_ptt: Option<_> = try { rating_as_fixed(compute_b30_ptt(system, &plays?)) }; let raw_score = play.scores.0[i].0; - query!( + conn.prepare_cached( " - INSERT INTO scores(play_id, score, creation_ptt, scoring_system) - VALUES ($1, $2, $3, $4) - ON CONFLICT(play_id, scoring_system) - DO UPDATE SET - score=$2, creation_ptt=$3 - WHERE play_id = $1 - AND scoring_system = $4 - + INSERT INTO scores(play_id, score, creation_ptt, scoring_system) + VALUES ($1, $2, $3, $4) + ON CONFLICT(play_id, scoring_system) + DO UPDATE SET + score=$2, creation_ptt=$3 + WHERE play_id = $1 + AND scoring_system = $4 ", + )? + .execute(( play.id, raw_score, creation_ptt, ScoringSystem::SCORING_SYSTEM_DB_STRINGS[i], - ) - .execute(&ctx.db) - .await?; + ))?; } i += 1; diff --git a/src/commands/chart.rs b/src/commands/chart.rs index 71a8a29..b0e1518 100644 --- a/src/commands/chart.rs +++ b/src/commands/chart.rs @@ -1,10 +1,9 @@ use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage}; -use sqlx::query; use crate::{ - arcaea::chart::Side, + arcaea::{chart::Side, play::Play}, context::{Context, Error}, - get_user, play_from_db_record, + get_user, recognition::fuzzy_song_name::guess_song_and_chart, }; use std::io::Cursor; @@ -23,7 +22,7 @@ use poise::CreateReply; use crate::{ arcaea::score::{Score, ScoringSystem}, - user::discord_it_to_discord_user, + user::discord_id_to_discord_user, }; // {{{ Top command @@ -55,16 +54,18 @@ async fn info( None => None, }; - let play_count = query!( - " + let play_count: usize = ctx + .data() + .db + .get()? + .prepare_cached( + " SELECT COUNT(*) as count FROM plays WHERE chart_id=? - ", - chart.id - ) - .fetch_one(&ctx.data().db) - .await?; + ", + )? + .query_row([chart.id], |row| row.get(0))?; let mut embed = CreateEmbed::default() .title(format!( @@ -77,7 +78,7 @@ async fn info( format!("{:.1}", chart.chart_constant as f32 / 100.0), true, ) - .field("Total plays", format!("{}", play_count.count), true) + .field("Total plays", format!("{play_count}"), true) .field("BPM", &song.bpm, true) .field("Side", Side::SIDE_STRINGS[song.side.to_index()], true) .field("Artist", &song.title, true); @@ -117,42 +118,40 @@ async fn best( let user = get_user!(&ctx); let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?; - let play = query!( - " - SELECT + let play = ctx + .data() + .db + .get()? + .prepare_cached( + " + SELECT p.id, p.chart_id, p.user_id, p.created_at, p.max_recall, p.far_notes, s.score - FROM plays p - JOIN scores s ON s.play_id = p.id - WHERE s.scoring_system='standard' - AND p.user_id=? - AND p.chart_id=? - ORDER BY s.score DESC - LIMIT 1 - ", - user.id, - chart.id - ) - .fetch_one(&ctx.data().db) - .await - .map_err(|_| { - format!( - "Could not find any scores for {} [{:?}]", - song.title, chart.difficulty - ) - })?; - let play = play_from_db_record!(chart, play); + FROM plays p + JOIN scores s ON s.play_id = p.id + WHERE s.scoring_system='standard' + AND p.user_id=? + AND p.chart_id=? + ORDER BY s.score DESC + LIMIT 1 + ", + )? + .query_row((user.id, chart.id), |row| Play::from_sql(chart, row)) + .map_err(|_| { + format!( + "Could not find any scores for {} [{:?}]", + song.title, chart.difficulty + ) + })?; - let (embed, attachment) = play - .to_embed( - &ctx.data().db, - &user, - &song, - &chart, - 0, - Some(&discord_it_to_discord_user(&ctx, &user.discord_id).await?), - ) - .await?; + let (embed, attachment) = play.to_embed( + ctx.data(), + &user, + song, + chart, + 0, + Some(&discord_id_to_discord_user(&ctx, &user.discord_id).await?), + )?; ctx.channel_id() .send_files(ctx.http(), attachment, CreateMessage::new().embed(embed)) @@ -177,8 +176,12 @@ async fn plot( let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?; // SAFETY: we limit the amount of plotted plays to 1000. - let plays = query!( - " + let plays = ctx + .data() + .db + .get()? + .prepare_cached( + " SELECT p.id, p.chart_id, p.user_id, p.created_at, p.max_recall, p.far_notes, s.score @@ -190,11 +193,9 @@ async fn plot( ORDER BY s.score DESC LIMIT 1000 ", - user.id, - chart.id - ) - .fetch_all(&ctx.data().db) - .await?; + )? + .query_map((user.id, chart.id), |row| Play::from_sql(chart, row))? + .collect::, _>>()?; if plays.len() == 0 { ctx.reply(format!( @@ -209,7 +210,7 @@ async fn plot( let max_time = plays.iter().map(|p| p.created_at).max().unwrap(); let mut min_score = plays .iter() - .map(|p| play_from_db_record!(chart, p).score(scoring_system)) + .map(|p| p.score(scoring_system)) .min() .unwrap() .0 as i64; @@ -266,7 +267,7 @@ async fn plot( .map(|play| { ( play.created_at.and_utc().timestamp_millis(), - play_from_db_record!(chart, play).score(scoring_system), + play.score(scoring_system), ) }) .collect(); diff --git a/src/commands/score.rs b/src/commands/score.rs index efa6b23..8971eb7 100644 --- a/src/commands/score.rs +++ b/src/commands/score.rs @@ -1,16 +1,15 @@ use std::time::Instant; -use crate::arcaea::play::CreatePlay; +use crate::arcaea::play::{CreatePlay, Play}; use crate::arcaea::score::Score; use crate::context::{Context, Error}; use crate::recognition::recognize::{ImageAnalyzer, ScoreKind}; -use crate::user::{discord_it_to_discord_user, User}; -use crate::{edit_reply, get_user, play_from_db_record, timed}; +use crate::user::{discord_id_to_discord_user, User}; +use crate::{edit_reply, get_user, timed}; use image::DynamicImage; use poise::serenity_prelude::futures::future::join_all; use poise::serenity_prelude::CreateMessage; use poise::{serenity_prelude as serenity, CreateReply}; -use sqlx::query; // {{{ Score /// Score management @@ -121,15 +120,13 @@ pub async fn magic( .with_attachment(file) .with_fars(maybe_fars) .with_max_recall(max_recall) - .save(&ctx.data(), &user, &chart) - .await?; + .save(&ctx.data(), &user, &chart)?; // }}} // }}} // {{{ Deliver embed let (embed, attachment) = timed!("to embed", { - play.to_embed(&ctx.data().db, &user, &song, &chart, i, None) - .await? + play.to_embed(ctx.data(), &user, &song, &chart, i, None)? }); embeds.push(embed); @@ -183,11 +180,14 @@ pub async fn delete( let mut count = 0; for id in ids { - let res = query!("DELETE FROM plays WHERE id=? AND user_id=?", id, user.id) - .execute(&ctx.data().db) - .await?; + let res = ctx + .data() + .db + .get()? + .prepare_cached("DELETE FROM plays WHERE id=? AND user_id=?")? + .execute((id, user.id))?; - if res.rows_affected() == 0 { + if res == 0 { ctx.reply(format!("No play with id {} found", id)).await?; } else { count += 1; @@ -216,36 +216,38 @@ pub async fn show( let mut embeds = Vec::with_capacity(ids.len()); let mut attachments = Vec::with_capacity(ids.len()); + let conn = ctx.data().db.get()?; for (i, id) in ids.iter().enumerate() { - let res = query!( - " - SELECT - p.id, p.chart_id, p.user_id, p.created_at, - p.max_recall, p.far_notes, s.score, - u.discord_id - FROM plays p - JOIN scores s ON s.play_id = p.id - JOIN users u ON p.user_id = u.id - WHERE s.scoring_system='standard' - AND p.id=? - ORDER BY s.score DESC - LIMIT 1 - ", - id - ) - .fetch_one(&ctx.data().db) - .await - .map_err(|_| format!("Could not find play with id {}", id))?; + let (song, chart, play, discord_id) = conn + .prepare_cached( + " + SELECT + p.id, p.chart_id, p.user_id, p.created_at, + p.max_recall, p.far_notes, s.score, + u.discord_id + FROM plays p + JOIN scores s ON s.play_id = p.id + JOIN users u ON p.user_id = u.id + WHERE s.scoring_system='standard' + AND p.id=? + ORDER BY s.score DESC + LIMIT 1 + ", + )? + .query_and_then([id], |row| -> Result<_, Error> { + let (song, chart) = ctx.data().song_cache.lookup_chart(row.get("chart_id")?)?; + let play = Play::from_sql(chart, row)?; + let discord_id = row.get::<_, String>("discord_id")?; + Ok((song, chart, play, discord_id)) + })? + .next() + .ok_or_else(|| format!("Could not find play with id {}", id))??; - let (song, chart) = ctx.data().song_cache.lookup_chart(res.chart_id as u32)?; - let play = play_from_db_record!(chart, res); + let author = discord_id_to_discord_user(&ctx, &discord_id).await?; + let user = User::by_id(ctx.data(), play.user_id)?; - let author = discord_it_to_discord_user(&ctx, &res.discord_id).await?; - let user = User::by_id(&ctx.data().db, play.user_id).await?; - - let (embed, attachment) = play - .to_embed(&ctx.data().db, &user, song, chart, i, Some(&author)) - .await?; + let (embed, attachment) = + play.to_embed(ctx.data(), &user, song, chart, i, Some(&author))?; embeds.push(embed); attachments.extend(attachment); diff --git a/src/commands/stats.rs b/src/commands/stats.rs index 3cbc6da..a5d1b8a 100644 --- a/src/commands/stats.rs +++ b/src/commands/stats.rs @@ -5,7 +5,6 @@ use poise::{ serenity_prelude::{CreateAttachment, CreateEmbed}, CreateReply, }; -use sqlx::query; use crate::{ arcaea::{ @@ -26,7 +25,7 @@ use crate::{ context::{Context, Error}, get_user, logs::debug_image_log, - reply_errors, + reply_errors, timed, user::User, }; @@ -53,20 +52,20 @@ async fn best_plays( let user_ctx = ctx.data(); let plays = reply_errors!( ctx, - get_best_plays( - &user_ctx.db, - &user_ctx.song_cache, - user.id, - scoring_system, - if require_full { - grid_size.0 * grid_size.1 - } else { - grid_size.0 * (grid_size.1.max(1) - 1) + 1 - } as usize, - (grid_size.0 * grid_size.1) as usize, - None - ) - .await? + timed!("get_best_plays", { + get_best_plays( + user_ctx, + user.id, + scoring_system, + if require_full { + grid_size.0 * grid_size.1 + } else { + grid_size.0 * (grid_size.1.max(1) - 1) + 1 + } as usize, + (grid_size.0 * grid_size.1) as usize, + None, + )? + }) ); // {{{ Layout @@ -463,48 +462,42 @@ pub async fn bany( #[poise::command(prefix_command, slash_command, user_cooldown = 1)] async fn meta(ctx: Context<'_>) -> Result<(), Error> { let user = get_user!(&ctx); - let song_count = query!("SELECT count() as count FROM songs") - .fetch_one(&ctx.data().db) - .await? - .count; + let conn = ctx.data().db.get()?; + let song_count: usize = conn + .prepare_cached("SELECT count() as count FROM songs")? + .query_row((), |row| row.get(0))?; - let chart_count = query!("SELECT count() as count FROM charts") - .fetch_one(&ctx.data().db) - .await? - .count; + let chart_count: usize = conn + .prepare_cached("SELECT count() as count FROM charts")? + .query_row((), |row| row.get(0))?; - let users_count = query!("SELECT count() as count FROM users") - .fetch_one(&ctx.data().db) - .await? - .count; + let users_count: usize = conn + .prepare_cached("SELECT count() as count FROM users")? + .query_row((), |row| row.get(0))?; - let pookie_count = query!( - " - SELECT count() as count - FROM users - WHERE is_pookie=1 - " - ) - .fetch_one(&ctx.data().db) - .await? - .count; + let pookie_count: usize = conn + .prepare_cached( + " + SELECT count() as count + FROM users + WHERE is_pookie=1 + ", + )? + .query_row((), |row| row.get(0))?; - let play_count = query!("SELECT count() as count FROM plays") - .fetch_one(&ctx.data().db) - .await? - .count; + let play_count: usize = conn + .prepare_cached("SELECT count() as count FROM plays")? + .query_row((), |row| row.get(0))?; - let your_play_count = query!( - " + let your_play_count: usize = conn + .prepare_cached( + " SELECT count() as count FROM plays WHERE user_id=? - ", - user.id - ) - .fetch_one(&ctx.data().db) - .await? - .count; + ", + )? + .query_row([user.id], |row| row.get(0))?; let embed = CreateEmbed::default() .title("Bot statistics") diff --git a/src/commands/utils/mod.rs b/src/commands/utils/mod.rs index 599a60b..beda3af 100644 --- a/src/commands/utils/mod.rs +++ b/src/commands/utils/mod.rs @@ -14,7 +14,7 @@ macro_rules! edit_reply { #[macro_export] macro_rules! get_user { ($ctx:expr) => {{ - crate::reply_errors!($ctx, crate::user::User::from_context($ctx).await) + crate::reply_errors!($ctx, crate::user::User::from_context($ctx)) }}; } diff --git a/src/context.rs b/src/context.rs index 91ceb64..45b5a84 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,7 +1,6 @@ +use r2d2_sqlite::SqliteConnectionManager; use std::fs; -use sqlx::SqlitePool; - use crate::{ arcaea::{chart::SongCache, jacket::JacketCache}, assets::{get_data_dir, EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT}, @@ -13,9 +12,11 @@ use crate::{ pub type Error = Box; pub type Context<'a> = poise::Context<'a, UserContext, Error>; +pub type DbConnection = r2d2::Pool; + // Custom user data passed to all command functions pub struct UserContext { - pub db: SqlitePool, + pub db: DbConnection, pub song_cache: SongCache, pub jacket_cache: JacketCache, pub ui_measurements: UIMeasurements, @@ -29,11 +30,11 @@ pub struct UserContext { impl UserContext { #[inline] - pub async fn new(db: SqlitePool) -> Result { + pub async fn new(db: DbConnection) -> Result { timed!("create_context", { fs::create_dir_all(get_data_dir())?; - let mut song_cache = timed!("make_song_cache", { SongCache::new(&db).await? }); + let mut song_cache = timed!("make_song_cache", { SongCache::new(&db)? }); let jacket_cache = timed!("make_jacket_cache", { JacketCache::new(&mut song_cache)? }); let ui_measurements = timed!("read_ui_measurements", { UIMeasurements::read()? }); diff --git a/src/main.rs b/src/main.rs index d93124e..a0b547b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,9 +22,16 @@ mod user; use arcaea::play::generate_missing_scores; use assets::get_data_dir; use context::{Error, UserContext}; +use include_dir::{include_dir, Dir}; use poise::serenity_prelude::{self as serenity}; -use sqlx::sqlite::SqlitePoolOptions; -use std::{env::var, sync::Arc, time::Duration}; +use r2d2::Pool; +use r2d2_sqlite::SqliteConnectionManager; +use rusqlite_migration::Migrations; +use std::{ + env::var, + sync::{Arc, LazyLock}, + time::Duration, +}; // {{{ Error handler async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) { @@ -40,13 +47,27 @@ async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) { #[tokio::main] async fn main() { - let pool = SqlitePoolOptions::new() - .connect(&format!( - "sqlite://{}/db.sqlite", - get_data_dir().to_str().unwrap() - )) - .await - .unwrap(); + let pool = Pool::new( + SqliteConnectionManager::file(&format!("{}/db.sqlite", get_data_dir().to_str().unwrap())) + .with_init(|conn| { + static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); + static MIGRATIONS: LazyLock = LazyLock::new(|| { + timed!("create_migration_structure", { + Migrations::from_directory(&MIGRATIONS_DIR) + .expect("Could not load migrations") + }) + }); + + timed!("run_migrations", { + MIGRATIONS + .to_latest(conn) + .expect("Could not run migrations"); + }); + + Ok(()) + }), + ) + .expect("Could not open sqlite database."); // {{{ Poise options let options = poise::FrameworkOptions { diff --git a/src/user.rs b/src/user.rs index c8d38b2..111b127 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,9 +1,9 @@ use std::str::FromStr; use poise::serenity_prelude::UserId; -use sqlx::SqlitePool; +use rusqlite::Row; -use crate::context::{Context, Error}; +use crate::context::{Context, Error, UserContext}; #[derive(Debug, Clone)] pub struct User { @@ -13,35 +13,44 @@ pub struct User { } impl User { - pub async fn from_context(ctx: &Context<'_>) -> Result { - let id = ctx.author().id.get().to_string(); - let user = sqlx::query!("SELECT * FROM users WHERE discord_id = ?", id) - .fetch_one(&ctx.data().db) - .await - .map_err(|_| "You are not an user in my database, sowwy ^~^")?; - - Ok(User { - id: user.id as u32, - discord_id: user.discord_id, - is_pookie: user.is_pookie, + #[inline] + fn from_row<'a, 'b>(row: &'a Row<'b>) -> Result { + Ok(Self { + id: row.get("id")?, + discord_id: row.get("discord_id")?, + is_pookie: row.get("is_pookie")?, }) } - pub async fn by_id(db: &SqlitePool, id: u32) -> Result { - let user = sqlx::query!("SELECT * FROM users WHERE id = ?", id) - .fetch_one(db) - .await?; + pub fn from_context(ctx: &Context<'_>) -> Result { + let id = ctx.author().id.get().to_string(); + let user = ctx + .data() + .db + .get()? + .prepare_cached("SELECT * FROM users WHERE discord_id = ?")? + .query_map([id], Self::from_row)? + .next() + .ok_or_else(|| "You are not an user in my database, sowwy ^~^")??; - Ok(User { - id: user.id as u32, - discord_id: user.discord_id, - is_pookie: user.is_pookie, - }) + Ok(user) + } + + pub fn by_id(ctx: &UserContext, id: u32) -> Result { + let user = ctx + .db + .get()? + .prepare_cached("SELECT * FROM users WHERE id = ?")? + .query_map([id], Self::from_row)? + .next() + .ok_or_else(|| "You are not an user in my database, sowwy ^~^")??; + + Ok(user) } } #[inline] -pub async fn discord_it_to_discord_user( +pub async fn discord_id_to_discord_user( &ctx: &Context<'_>, discord_id: &str, ) -> Result {