1
Fork 0

Implement migrations, and switch from sqlx to rusqlite

This commit is contained in:
prescientmoon 2024-08-22 22:11:21 +02:00
parent 7cdc3a2755
commit fee7fe77f8
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
17 changed files with 629 additions and 1044 deletions

1
.gitignore vendored
View file

@ -10,3 +10,4 @@ shimmering/assets/b30_background.*
target target
backups backups
dump.sql dump.sql
schema.sql

686
Cargo.lock generated
View file

@ -60,12 +60,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
[[package]]
name = "allocator-api2"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@ -133,15 +127,6 @@ dependencies = [
"syn 2.0.66", "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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.3.0" version = "1.3.0"
@ -198,12 +183,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "bit_field" name = "bit_field"
version = "0.10.2" version = "0.10.2"
@ -218,12 +197,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.5.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bitstream-io" name = "bitstream-io"
@ -315,13 +291,13 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.99" version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
"once_cell", "shlex",
] ]
[[package]] [[package]]
@ -361,21 +337,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -437,21 +398,6 @@ dependencies = [
"libc", "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]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.4.2"
@ -489,15 +435,6 @@ dependencies = [
"crossbeam-utils", "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]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.20" version = "0.8.20"
@ -585,17 +522,6 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" 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]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -624,9 +550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"const-oid",
"crypto-common", "crypto-common",
"subtle",
] ]
[[package]] [[package]]
@ -659,12 +583,6 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]] [[package]]
name = "dwrote" name = "dwrote"
version = "0.11.0" version = "0.11.0"
@ -682,9 +600,6 @@ name = "either"
version = "1.12.0" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
@ -720,28 +635,6 @@ dependencies = [
"version_check", "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]] [[package]]
name = "exr" name = "exr"
version = "1.72.0" version = "1.72.0"
@ -758,6 +651,18 @@ dependencies = [
"zune-inflate", "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]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.1.0" version = "2.1.0"
@ -795,9 +700,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
dependencies = [ dependencies = [
"futures-core", "spin",
"futures-sink",
"spin 0.9.8",
] ]
[[package]] [[package]]
@ -812,7 +715,7 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2845a73bbd781e691ab7c2a028c579727cd254942e8ced57ff73e0eafd60de87" checksum = "2845a73bbd781e691ab7c2a028c579727cd254942e8ced57ff73e0eafd60de87"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.6.0",
"byteorder", "byteorder",
"core-foundation", "core-foundation",
"core-graphics", "core-graphics",
@ -873,7 +776,7 @@ version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5442dee36ca09604133580dc0553780e867936bb3cbef3275859e889026d2b17" checksum = "5442dee36ca09604133580dc0553780e867936bb3cbef3275859e889026d2b17"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.6.0",
"freetype-sys", "freetype-sys",
"libc", "libc",
] ]
@ -919,28 +822,6 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 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]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.30" version = "0.3.30"
@ -1088,7 +969,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [ dependencies = [
"ahash", "ahash",
"allocator-api2",
] ]
[[package]] [[package]]
@ -1112,39 +992,6 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 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]] [[package]]
name = "http" name = "http"
version = "0.2.12" version = "0.2.12"
@ -1348,6 +1195,25 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" 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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.2.6" version = "2.2.6"
@ -1419,9 +1285,6 @@ name = "lazy_static"
version = "1.4.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
dependencies = [
"spin 0.5.2",
]
[[package]] [[package]]
name = "lebe" name = "lebe"
@ -1468,15 +1331,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.6.0",
"libc", "libc",
] ]
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.28.0" version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",
@ -1534,16 +1397,6 @@ dependencies = [
"rayon", "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]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@ -1669,23 +1522,6 @@ dependencies = [
"num-traits", "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]] [[package]]
name = "num-complex" name = "num-complex"
version = "0.4.6" version = "0.4.6"
@ -1799,12 +1635,6 @@ dependencies = [
"ttf-parser 0.24.1", "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]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.3" version = "0.12.3"
@ -1823,7 +1653,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.5.2", "redox_syscall",
"smallvec", "smallvec",
"windows-targets 0.52.5", "windows-targets 0.52.5",
] ]
@ -1853,15 +1683,6 @@ dependencies = [
"rustc_version", "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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -1880,27 +1701,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 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]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.30" version = "0.3.30"
@ -2037,7 +1837,7 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.6.0",
"memchr", "memchr",
"unicase", "unicase",
] ]
@ -2066,6 +1866,28 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -2182,22 +2004,13 @@ dependencies = [
"crossbeam-utils", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.2" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.6.0",
] ]
[[package]] [[package]]
@ -2303,29 +2116,35 @@ dependencies = [
"cfg-if", "cfg-if",
"getrandom", "getrandom",
"libc", "libc",
"spin 0.9.8", "spin",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
name = "rsa" name = "rusqlite"
version = "0.9.6" version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
dependencies = [ dependencies = [
"const-oid", "bitflags 2.6.0",
"digest", "chrono",
"num-bigint-dig", "fallible-iterator",
"num-integer", "fallible-streaming-iterator",
"num-traits", "hashlink",
"pkcs1", "libsqlite3-sys",
"pkcs8", "smallvec",
"rand_core", ]
"signature",
"spki", [[package]]
"subtle", name = "rusqlite_migration"
"zeroize", 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]] [[package]]
@ -2349,7 +2168,7 @@ version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.6.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@ -2442,6 +2261,15 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -2547,7 +2375,7 @@ dependencies = [
"arrayvec", "arrayvec",
"async-trait", "async-trait",
"base64 0.22.1", "base64 0.22.1",
"bitflags 2.5.0", "bitflags 2.6.0",
"bytes", "bytes",
"chrono", "chrono",
"dashmap", "dashmap",
@ -2582,17 +2410,6 @@ dependencies = [
"digest", "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]] [[package]]
name = "shimmeringmoon" name = "shimmeringmoon"
version = "0.1.0" version = "0.1.0"
@ -2602,22 +2419,22 @@ dependencies = [
"hypertesseract", "hypertesseract",
"image 0.25.2", "image 0.25.2",
"imageproc", "imageproc",
"include_dir",
"num", "num",
"plotters", "plotters",
"poise", "poise",
"sqlx", "r2d2",
"r2d2_sqlite",
"rusqlite",
"rusqlite_migration",
"tokio", "tokio",
] ]
[[package]] [[package]]
name = "signature" name = "shlex"
version = "2.2.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
dependencies = [
"digest",
"rand_core",
]
[[package]] [[package]]
name = "simba" name = "simba"
@ -2676,9 +2493,6 @@ name = "smallvec"
version = "1.13.2" version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
@ -2690,12 +2504,6 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@ -2705,235 +2513,6 @@ dependencies = [
"lock_api", "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]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@ -3176,17 +2755,6 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.21.0" version = "0.21.0"
@ -3404,18 +2972,6 @@ dependencies = [
"tinyvec", "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]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@ -3440,6 +2996,16 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 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]] [[package]]
name = "v_frame" name = "v_frame"
version = "0.3.8" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.92" version = "0.2.92"
@ -3610,16 +3170,6 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" 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]] [[package]]
name = "wide" name = "wide"
version = "0.7.26" version = "0.7.26"

View file

@ -10,10 +10,14 @@ image = "0.25.2"
num = "0.4.3" num = "0.4.3"
plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c", features=["bitmap_backend"] } plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c", features=["bitmap_backend"] }
poise = "0.6.1" 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" } hypertesseract = { features=["image"], git="https://github.com/BlueGhostGH/hypertesseract.git", rev="4e05063" }
tokio = {version="1.38.0", features=["rt-multi-thread"]} tokio = {version="1.38.0", features=["rt-multi-thread"]}
imageproc = "0.25.0" 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."*"] [profile.dev.package."*"]
opt-level = 3 opt-level = 3

View file

@ -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)
)
-- }}}

View file

@ -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)
);
-- }}}

View file

@ -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)
)
-- }}}

View file

@ -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);

View file

@ -1,5 +1,4 @@
use image::RgbaImage; use image::RgbaImage;
use sqlx::query;
use crate::{ use crate::{
assets::get_data_dir, assets::get_data_dir,
@ -122,16 +121,8 @@ impl GoalStats {
user: &User, user: &User,
scoring_system: ScoringSystem, scoring_system: ScoringSystem,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let plays = get_best_plays( let plays = get_best_plays(ctx, user.id, scoring_system, 0, usize::MAX, None)??;
&ctx.db, let conn = ctx.db.get()?;
&ctx.song_cache,
user.id,
scoring_system,
0,
usize::MAX,
None,
)
.await??;
// {{{ PM count // {{{ PM count
let pm_count = plays let pm_count = plays
@ -142,30 +133,31 @@ impl GoalStats {
.count(); .count();
// }}} // }}}
// {{{ Play count // {{{ Play count
let play_count = query!( let play_count = conn
"SELECT count() as count FROM plays WHERE user_id=?", .prepare_cached("SELECT count() as count FROM plays WHERE user_id=?")?
user.id .query_row([user.id], |row| row.get(0))?;
)
.fetch_one(&ctx.db)
.await?
.count as usize;
// }}} // }}}
// {{{ Peak ptt // {{{ Peak ptt
let peak_ptt = query!( let peak_ptt = conn
" .prepare_cached(
"
SELECT s.creation_ptt SELECT s.creation_ptt
FROM plays p FROM plays p
JOIN scores s ON s.play_id = p.id JOIN scores s ON s.play_id = p.id
WHERE user_id = ? WHERE user_id = ?
AND scoring_system = ? AND scoring_system = ?
ORDER BY s.creation_ptt DESC
LIMIT 1
", ",
user.id, )?
ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()] .query_row(
) (
.fetch_one(&ctx.db) user.id,
.await? ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()],
.creation_ptt ),
.ok_or_else(|| "No ptt history data found")? as u32; |row| row.get(0),
)
.map_err(|_| "No ptt history data found")?;
// }}} // }}}
// {{{ Peak PM relay // {{{ Peak PM relay
let peak_pm_relay = { let peak_pm_relay = {

View file

@ -1,12 +1,15 @@
use std::{fmt::Display, num::NonZeroU16, path::PathBuf}; use std::{fmt::Display, num::NonZeroU16, path::PathBuf};
use image::{ImageBuffer, Rgb}; 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 // {{{ Difficuly
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Difficulty { pub enum Difficulty {
PST, PST,
PRS, PRS,
@ -29,17 +32,19 @@ impl Difficulty {
} }
} }
impl TryFrom<String> for Difficulty { impl FromSql for Difficulty {
type Error = String; fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
let str: String = rusqlite::types::FromSql::column_result(value)?;
fn try_from(value: String) -> Result<Self, Self::Error> {
for (i, s) in Self::DIFFICULTY_SHORTHANDS.iter().enumerate() { for (i, s) in Self::DIFFICULTY_SHORTHANDS.iter().enumerate() {
if value == **s { if str == **s {
return Ok(Self::DIFFICULTIES[i]); 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<String> for Level { impl FromSql for Level {
type Error = String; fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
let str: String = rusqlite::types::FromSql::column_result(value)?;
fn try_from(value: String) -> Result<Self, Self::Error> {
for (i, s) in Self::LEVEL_STRINGS.iter().enumerate() { for (i, s) in Self::LEVEL_STRINGS.iter().enumerate() {
if value == **s { if str == **s {
return Ok(Self::LEVELS[i]); 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<String> for Side { impl FromSql for Side {
type Error = String; fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
let str: String = rusqlite::types::FromSql::column_result(value)?;
fn try_from(value: String) -> Result<Self, Self::Error> {
for (i, s) in Self::SIDE_STRINGS.iter().enumerate() { for (i, s) in Self::SIDE_STRINGS.iter().enumerate() {
if value == **s { if str == **s {
return Ok(Self::SIDES[i]); 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 // {{{ Populate cache
pub async fn new(pool: &SqlitePool) -> Result<Self, Error> { pub fn new(conn: &DbConnection) -> Result<Self, Error> {
let conn = conn.get()?;
let mut result = Self::default(); let mut result = Self::default();
// {{{ Songs // {{{ Songs
let songs = sqlx::query!("SELECT * FROM songs").fetch_all(pool).await?; let mut query = conn.prepare_cached("SELECT * FROM songs")?;
for song in songs { let songs = query.query_map((), |row| {
let song = Song { Ok(Song {
id: song.id as u32, id: row.get("id")?,
lowercase_title: song.title.to_lowercase(), lowercase_title: row.get::<_, String>("title")?.to_lowercase(),
title: song.title, title: row.get("title")?,
artist: song.artist, artist: row.get("artist")?,
pack: song.pack, pack: row.get("pack")?,
bpm: song.bpm, bpm: row.get("bpm")?,
side: Side::try_from(song.side)?, side: row.get("side")?,
}; })
})?;
for song in songs {
let song = song?;
let song_id = song.id as usize; let song_id = song.id as usize;
if song_id >= result.songs.len() { if song_id >= result.songs.len() {
result.songs.resize(song_id + 1, None); result.songs.resize(song_id + 1, None);
} }
result.songs[song_id] = Some(CachedSong::new(song)); result.songs[song_id] = Some(CachedSong::new(song));
} }
// }}} // }}}
// {{{ Charts // {{{ Charts
let charts = sqlx::query!("SELECT * FROM charts").fetch_all(pool).await?; let mut query = conn.prepare_cached("SELECT * FROM charts")?;
for chart in charts { let charts = query.query_map((), |row| {
let chart = Chart { Ok(Chart {
id: chart.id as u32, id: row.get("id")?,
song_id: chart.song_id as u32, song_id: row.get("song_id")?,
shorthand: chart.shorthand, shorthand: row.get("shorthand")?,
difficulty: Difficulty::try_from(chart.difficulty)?, difficulty: row.get("difficulty")?,
level: Level::try_from(chart.level)?, level: row.get("level")?,
chart_constant: chart.chart_constant as u32, chart_constant: row.get("chart_constant")?,
note_count: chart.note_count as u32, note_count: row.get("note_count")?,
note_design: row.get("note_design")?,
cached_jacket: None, cached_jacket: None,
note_design: chart.note_design, })
}; })?;
for chart in charts {
let chart = chart?;
// {{{ Tie chart to song // {{{ Tie chart to song
{ {

View file

@ -9,14 +9,12 @@ use num::Zero;
use poise::serenity_prelude::{ use poise::serenity_prelude::{
Attachment, AttachmentId, CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp, Attachment, AttachmentId, CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp,
}; };
use sqlx::query_as; use rusqlite::Row;
use sqlx::{query, SqlitePool};
use crate::arcaea::chart::{Chart, Song}; use crate::arcaea::chart::{Chart, Song};
use crate::context::{Error, UserContext}; use crate::context::{Error, UserContext};
use crate::user::User; use crate::user::User;
use super::chart::SongCache;
use super::rating::{rating_as_fixed, rating_as_float}; use super::rating::{rating_as_fixed, rating_as_float};
use super::score::{Score, ScoringSystem}; use super::score::{Score, ScoringSystem};
@ -61,12 +59,14 @@ impl CreatePlay {
} }
// {{{ Save // {{{ Save
pub async fn save(self, ctx: &UserContext, user: &User, chart: &Chart) -> Result<Play, Error> { pub fn save(self, ctx: &UserContext, user: &User, chart: &Chart) -> Result<Play, Error> {
let conn = ctx.db.get()?;
let attachment_id = self.discord_attachment_id.map(|i| i.get() as i64); let attachment_id = self.discord_attachment_id.map(|i| i.get() as i64);
// {{{ Save current data to play // {{{ Save current data to play
let play = sqlx::query!( let (id, created_at) = conn
" .prepare_cached(
"
INSERT INTO plays( INSERT INTO plays(
user_id,chart_id,discord_attachment_id, user_id,chart_id,discord_attachment_id,
max_recall,far_notes max_recall,far_notes
@ -74,88 +74,55 @@ impl CreatePlay {
VALUES(?,?,?,?,?) VALUES(?,?,?,?,?)
RETURNING id, created_at RETURNING id, created_at
", ",
user.id, )?
chart.id, .query_row(
attachment_id, (
self.max_recall, user.id,
self.far_notes chart.id,
) attachment_id,
.fetch_one(&ctx.db) self.max_recall,
.await?; self.far_notes,
),
|row| Ok((row.get("id")?, row.get("created_at")?)),
)?;
// }}} // }}}
// {{{ Update creation ptt data // {{{ Update creation ptt data
let scores = ScoreCollection::from_standard_score(self.score, chart); let scores = ScoreCollection::from_standard_score(self.score, chart);
for system in ScoringSystem::SCORING_SYSTEMS { for system in ScoringSystem::SCORING_SYSTEMS {
let i = system.to_index(); let i = system.to_index();
let plays = get_best_plays(&ctx.db, &ctx.song_cache, user.id, system, 30, 30, None) let plays = get_best_plays(ctx, user.id, system, 30, 30, None)?.ok();
.await?
.ok();
let creation_ptt: Option<_> = try { rating_as_fixed(compute_b30_ptt(system, &plays?)) }; 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) INSERT INTO scores(play_id, score, creation_ptt, scoring_system)
VALUES (?,?,?,?) VALUES (?,?,?,?)
", ",
play.id, )?
.execute((
id,
scores.0[i].0, scores.0[i].0,
creation_ptt, creation_ptt,
ScoringSystem::SCORING_SYSTEM_DB_STRINGS[i] ScoringSystem::SCORING_SYSTEM_DB_STRINGS[i],
) ))?;
.execute(&ctx.db)
.await?;
} }
// }}} // }}}
Ok(Play { Ok(Play {
id: play.id as u32, id,
created_at: play.created_at, created_at,
scores,
chart_id: chart.id, chart_id: chart.id,
user_id: user.id, user_id: user.id,
scores,
max_recall: self.max_recall, max_recall: self.max_recall,
far_notes: self.far_notes, 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<i64>,
pub far_notes: Option<i64>,
pub score: i64,
}
// }}} // }}}
// {{{ Score data // {{{ Score data
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -185,6 +152,20 @@ pub struct Play {
} }
impl Play { impl Play {
// {{{ Row parsing
#[inline]
pub fn from_sql(chart: &Chart, row: &Row) -> Result<Self, rusqlite::Error> {
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 // {{{ Query the underlying score
#[inline] #[inline]
pub fn score(&self, system: ScoringSystem) -> Score { pub fn score(&self, system: ScoringSystem) -> Score {
@ -272,9 +253,9 @@ impl Play {
/// Creates a discord embed for this play. /// Creates a discord embed for this play.
/// ///
/// The `index` variable is only used to create distinct filenames. /// The `index` variable is only used to create distinct filenames.
pub async fn to_embed( pub fn to_embed(
&self, &self,
db: &SqlitePool, ctx: &UserContext,
user: &User, user: &User,
song: &Song, song: &Song,
chart: &Chart, chart: &Chart,
@ -282,33 +263,28 @@ impl Play {
author: Option<&poise::serenity_prelude::User>, author: Option<&poise::serenity_prelude::User>,
) -> Result<(CreateEmbed, Option<CreateAttachment>), Error> { ) -> Result<(CreateEmbed, Option<CreateAttachment>), Error> {
// {{{ Get previously best score // {{{ Get previously best score
let prev_play = query!( let prev_play = ctx
" .db
SELECT .get()?
p.id, p.chart_id, p.user_id, p.created_at, .prepare_cached(
p.max_recall, p.far_notes, s.score "
FROM plays p SELECT
JOIN scores s ON s.play_id = p.id p.id, p.chart_id, p.user_id, p.created_at,
WHERE s.scoring_system='standard' p.max_recall, p.far_notes, s.score
AND p.user_id=? FROM plays p
AND p.chart_id=? JOIN scores s ON s.play_id = p.id
AND p.created_at<? WHERE s.scoring_system='standard'
ORDER BY s.score DESC AND p.user_id=?
LIMIT 1 AND p.chart_id=?
", AND p.created_at<?
user.id, ORDER BY s.score DESC
chart.id, LIMIT 1
self.created_at ",
) )?
.fetch_optional(db) .query_row((user.id, chart.id, self.created_at), |row| {
.await Self::from_sql(chart, row)
.map_err(|_| { })
format!( .ok();
"Could not find any scores for {} [{:?}]",
song.title, chart.difficulty
)
})?
.map(|p| play_from_db_record!(chart, p));
let prev_score = prev_play.as_ref().map(|p| p.score(ScoringSystem::Standard)); let prev_score = prev_play.as_ref().map(|p| p.score(ScoringSystem::Standard));
let prev_zeta_score = prev_play.as_ref().map(|p| p.score(ScoringSystem::EX)); let prev_zeta_score = prev_play.as_ref().map(|p| p.score(ScoringSystem::EX));
@ -408,38 +384,47 @@ impl Play {
// {{{ General functions // {{{ General functions
pub type PlayCollection<'a> = Vec<(Play, &'a Song, &'a Chart)>; pub type PlayCollection<'a> = Vec<(Play, &'a Song, &'a Chart)>;
pub async fn get_best_plays<'a>( pub fn get_best_plays<'a>(
db: &SqlitePool, ctx: &'a UserContext,
song_cache: &'a SongCache,
user_id: u32, user_id: u32,
scoring_system: ScoringSystem, scoring_system: ScoringSystem,
min_amount: usize, min_amount: usize,
max_amount: usize, max_amount: usize,
before: Option<NaiveDateTime>, before: Option<NaiveDateTime>,
) -> Result<Result<PlayCollection<'a>, String>, Error> { ) -> Result<Result<PlayCollection<'a>, String>, Error> {
let conn = ctx.db.get()?;
// {{{ DB data fetching // {{{ DB data fetching
let plays: Vec<DbPlay> = query_as( let mut plays = conn
" .prepare_cached(
SELECT "
p.id, p.chart_id, p.user_id, p.created_at, SELECT
p.max_recall, p.far_notes, s.score, p.id, p.chart_id, p.user_id, p.created_at,
MAX(s.score) as _cscore p.max_recall, p.far_notes, s.score,
-- ^ This is only here to make sqlite pick the correct row for the bare columns MAX(cs.score) as _cscore
FROM plays p -- ^ This is only here to make sqlite pick the correct row for the bare columns
JOIN scores s ON s.play_id = p.id FROM plays p
JOIN scores cs ON cs.play_id = p.id JOIN scores s ON s.play_id = p.id
WHERE s.scoring_system='standard' JOIN scores cs ON cs.play_id = p.id
AND cs.scoring_system=? WHERE s.scoring_system='standard'
AND p.user_id=? AND cs.scoring_system=?
AND p.created_at<=? AND p.user_id=?
GROUP BY p.chart_id AND p.created_at<=?
", GROUP BY p.chart_id
) ",
.bind(ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()]) )?
.bind(user_id) .query_and_then(
.bind(before.unwrap_or_else(|| Utc::now().naive_utc())) (
.fetch_all(db) ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()],
.await?; 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::<Result<Vec<_>, Error>>()?;
// }}} // }}}
if plays.len() < min_amount { if plays.len() < min_amount {
@ -450,17 +435,6 @@ pub async fn get_best_plays<'a>(
} }
// {{{ B30 computation // {{{ 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::<Result<Vec<_>, Error>>()?;
plays.sort_by_key(|(play, _, chart)| -play.play_rating(scoring_system, chart.chart_constant)); plays.sort_by_key(|(play, _, chart)| -play.play_rating(scoring_system, chart.chart_constant));
plays.truncate(max_amount); plays.truncate(max_amount);
// }}} // }}}
@ -480,7 +454,8 @@ pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_>
// }}} // }}}
// {{{ Maintenance functions // {{{ Maintenance functions
pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), Error> { 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 SELECT
p.id, p.chart_id, p.user_id, p.created_at, 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 JOIN scores s ON s.play_id = p.id
WHERE s.scoring_system='standard' WHERE s.scoring_system='standard'
ORDER BY p.created_at ASC ORDER BY p.created_at ASC
" ",
) )?;
// Can't use the stream based version because of db locking...
.fetch_all(&ctx.db) let plays = query.query_and_then((), |row| -> Result<_, Error> {
.await?; let (_, chart) = ctx.song_cache.lookup_chart(row.get("chart_id")?)?;
let play = Play::from_sql(chart, row)?;
Ok(play)
})?;
let mut i = 0; let mut i = 0;
for play in plays { for play in plays {
let (_, chart) = ctx.song_cache.lookup_chart(play.chart_id as u32)?; let play = play?;
let play = play_from_db_record!(chart, play);
for system in ScoringSystem::SCORING_SYSTEMS { for system in ScoringSystem::SCORING_SYSTEMS {
let i = system.to_index(); let i = system.to_index();
let plays = get_best_plays( let plays =
&ctx.db, get_best_plays(&ctx, play.user_id, system, 30, 30, Some(play.created_at))?.ok();
&ctx.song_cache,
play.user_id,
system,
30,
30,
Some(play.created_at),
)
.await?
.ok();
let creation_ptt: Option<_> = try { rating_as_fixed(compute_b30_ptt(system, &plays?)) }; let creation_ptt: Option<_> = try { rating_as_fixed(compute_b30_ptt(system, &plays?)) };
let raw_score = play.scores.0[i].0; let raw_score = play.scores.0[i].0;
query!( conn.prepare_cached(
" "
INSERT INTO scores(play_id, score, creation_ptt, scoring_system) INSERT INTO scores(play_id, score, creation_ptt, scoring_system)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
ON CONFLICT(play_id, scoring_system) ON CONFLICT(play_id, scoring_system)
DO UPDATE SET DO UPDATE SET
score=$2, creation_ptt=$3 score=$2, creation_ptt=$3
WHERE play_id = $1 WHERE play_id = $1
AND scoring_system = $4 AND scoring_system = $4
", ",
)?
.execute((
play.id, play.id,
raw_score, raw_score,
creation_ptt, creation_ptt,
ScoringSystem::SCORING_SYSTEM_DB_STRINGS[i], ScoringSystem::SCORING_SYSTEM_DB_STRINGS[i],
) ))?;
.execute(&ctx.db)
.await?;
} }
i += 1; i += 1;

View file

@ -1,10 +1,9 @@
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage}; use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage};
use sqlx::query;
use crate::{ use crate::{
arcaea::chart::Side, arcaea::{chart::Side, play::Play},
context::{Context, Error}, context::{Context, Error},
get_user, play_from_db_record, get_user,
recognition::fuzzy_song_name::guess_song_and_chart, recognition::fuzzy_song_name::guess_song_and_chart,
}; };
use std::io::Cursor; use std::io::Cursor;
@ -23,7 +22,7 @@ use poise::CreateReply;
use crate::{ use crate::{
arcaea::score::{Score, ScoringSystem}, arcaea::score::{Score, ScoringSystem},
user::discord_it_to_discord_user, user::discord_id_to_discord_user,
}; };
// {{{ Top command // {{{ Top command
@ -55,16 +54,18 @@ async fn info(
None => None, None => None,
}; };
let play_count = query!( let play_count: usize = ctx
" .data()
.db
.get()?
.prepare_cached(
"
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM plays FROM plays
WHERE chart_id=? WHERE chart_id=?
", ",
chart.id )?
) .query_row([chart.id], |row| row.get(0))?;
.fetch_one(&ctx.data().db)
.await?;
let mut embed = CreateEmbed::default() let mut embed = CreateEmbed::default()
.title(format!( .title(format!(
@ -77,7 +78,7 @@ async fn info(
format!("{:.1}", chart.chart_constant as f32 / 100.0), format!("{:.1}", chart.chart_constant as f32 / 100.0),
true, true,
) )
.field("Total plays", format!("{}", play_count.count), true) .field("Total plays", format!("{play_count}"), true)
.field("BPM", &song.bpm, true) .field("BPM", &song.bpm, true)
.field("Side", Side::SIDE_STRINGS[song.side.to_index()], true) .field("Side", Side::SIDE_STRINGS[song.side.to_index()], true)
.field("Artist", &song.title, true); .field("Artist", &song.title, true);
@ -117,42 +118,40 @@ async fn best(
let user = get_user!(&ctx); let user = get_user!(&ctx);
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?; let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
let play = query!( let play = ctx
" .data()
SELECT .db
.get()?
.prepare_cached(
"
SELECT
p.id, p.chart_id, p.user_id, p.created_at, p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score p.max_recall, p.far_notes, s.score
FROM plays p FROM plays p
JOIN scores s ON s.play_id = p.id JOIN scores s ON s.play_id = p.id
WHERE s.scoring_system='standard' WHERE s.scoring_system='standard'
AND p.user_id=? AND p.user_id=?
AND p.chart_id=? AND p.chart_id=?
ORDER BY s.score DESC ORDER BY s.score DESC
LIMIT 1 LIMIT 1
", ",
user.id, )?
chart.id .query_row((user.id, chart.id), |row| Play::from_sql(chart, row))
) .map_err(|_| {
.fetch_one(&ctx.data().db) format!(
.await "Could not find any scores for {} [{:?}]",
.map_err(|_| { song.title, chart.difficulty
format!( )
"Could not find any scores for {} [{:?}]", })?;
song.title, chart.difficulty
)
})?;
let play = play_from_db_record!(chart, play);
let (embed, attachment) = play let (embed, attachment) = play.to_embed(
.to_embed( ctx.data(),
&ctx.data().db, &user,
&user, song,
&song, chart,
&chart, 0,
0, Some(&discord_id_to_discord_user(&ctx, &user.discord_id).await?),
Some(&discord_it_to_discord_user(&ctx, &user.discord_id).await?), )?;
)
.await?;
ctx.channel_id() ctx.channel_id()
.send_files(ctx.http(), attachment, CreateMessage::new().embed(embed)) .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)?; let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
// SAFETY: we limit the amount of plotted plays to 1000. // SAFETY: we limit the amount of plotted plays to 1000.
let plays = query!( let plays = ctx
" .data()
.db
.get()?
.prepare_cached(
"
SELECT SELECT
p.id, p.chart_id, p.user_id, p.created_at, p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score p.max_recall, p.far_notes, s.score
@ -190,11 +193,9 @@ async fn plot(
ORDER BY s.score DESC ORDER BY s.score DESC
LIMIT 1000 LIMIT 1000
", ",
user.id, )?
chart.id .query_map((user.id, chart.id), |row| Play::from_sql(chart, row))?
) .collect::<Result<Vec<_>, _>>()?;
.fetch_all(&ctx.data().db)
.await?;
if plays.len() == 0 { if plays.len() == 0 {
ctx.reply(format!( ctx.reply(format!(
@ -209,7 +210,7 @@ async fn plot(
let max_time = plays.iter().map(|p| p.created_at).max().unwrap(); let max_time = plays.iter().map(|p| p.created_at).max().unwrap();
let mut min_score = plays let mut min_score = plays
.iter() .iter()
.map(|p| play_from_db_record!(chart, p).score(scoring_system)) .map(|p| p.score(scoring_system))
.min() .min()
.unwrap() .unwrap()
.0 as i64; .0 as i64;
@ -266,7 +267,7 @@ async fn plot(
.map(|play| { .map(|play| {
( (
play.created_at.and_utc().timestamp_millis(), play.created_at.and_utc().timestamp_millis(),
play_from_db_record!(chart, play).score(scoring_system), play.score(scoring_system),
) )
}) })
.collect(); .collect();

View file

@ -1,16 +1,15 @@
use std::time::Instant; use std::time::Instant;
use crate::arcaea::play::CreatePlay; use crate::arcaea::play::{CreatePlay, Play};
use crate::arcaea::score::Score; use crate::arcaea::score::Score;
use crate::context::{Context, Error}; use crate::context::{Context, Error};
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind}; use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
use crate::user::{discord_it_to_discord_user, User}; use crate::user::{discord_id_to_discord_user, User};
use crate::{edit_reply, get_user, play_from_db_record, timed}; use crate::{edit_reply, get_user, timed};
use image::DynamicImage; use image::DynamicImage;
use poise::serenity_prelude::futures::future::join_all; use poise::serenity_prelude::futures::future::join_all;
use poise::serenity_prelude::CreateMessage; use poise::serenity_prelude::CreateMessage;
use poise::{serenity_prelude as serenity, CreateReply}; use poise::{serenity_prelude as serenity, CreateReply};
use sqlx::query;
// {{{ Score // {{{ Score
/// Score management /// Score management
@ -121,15 +120,13 @@ pub async fn magic(
.with_attachment(file) .with_attachment(file)
.with_fars(maybe_fars) .with_fars(maybe_fars)
.with_max_recall(max_recall) .with_max_recall(max_recall)
.save(&ctx.data(), &user, &chart) .save(&ctx.data(), &user, &chart)?;
.await?;
// }}} // }}}
// }}} // }}}
// {{{ Deliver embed // {{{ Deliver embed
let (embed, attachment) = timed!("to embed", { let (embed, attachment) = timed!("to embed", {
play.to_embed(&ctx.data().db, &user, &song, &chart, i, None) play.to_embed(ctx.data(), &user, &song, &chart, i, None)?
.await?
}); });
embeds.push(embed); embeds.push(embed);
@ -183,11 +180,14 @@ pub async fn delete(
let mut count = 0; let mut count = 0;
for id in ids { for id in ids {
let res = query!("DELETE FROM plays WHERE id=? AND user_id=?", id, user.id) let res = ctx
.execute(&ctx.data().db) .data()
.await?; .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?; ctx.reply(format!("No play with id {} found", id)).await?;
} else { } else {
count += 1; count += 1;
@ -216,36 +216,38 @@ pub async fn show(
let mut embeds = Vec::with_capacity(ids.len()); let mut embeds = Vec::with_capacity(ids.len());
let mut attachments = 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() { for (i, id) in ids.iter().enumerate() {
let res = query!( let (song, chart, play, discord_id) = conn
" .prepare_cached(
SELECT "
p.id, p.chart_id, p.user_id, p.created_at, SELECT
p.max_recall, p.far_notes, s.score, p.id, p.chart_id, p.user_id, p.created_at,
u.discord_id p.max_recall, p.far_notes, s.score,
FROM plays p u.discord_id
JOIN scores s ON s.play_id = p.id FROM plays p
JOIN users u ON p.user_id = u.id JOIN scores s ON s.play_id = p.id
WHERE s.scoring_system='standard' JOIN users u ON p.user_id = u.id
AND p.id=? WHERE s.scoring_system='standard'
ORDER BY s.score DESC AND p.id=?
LIMIT 1 ORDER BY s.score DESC
", LIMIT 1
id ",
) )?
.fetch_one(&ctx.data().db) .query_and_then([id], |row| -> Result<_, Error> {
.await let (song, chart) = ctx.data().song_cache.lookup_chart(row.get("chart_id")?)?;
.map_err(|_| format!("Could not find play with id {}", 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 author = discord_id_to_discord_user(&ctx, &discord_id).await?;
let play = play_from_db_record!(chart, res); let user = User::by_id(ctx.data(), play.user_id)?;
let author = discord_it_to_discord_user(&ctx, &res.discord_id).await?; let (embed, attachment) =
let user = User::by_id(&ctx.data().db, play.user_id).await?; play.to_embed(ctx.data(), &user, song, chart, i, Some(&author))?;
let (embed, attachment) = play
.to_embed(&ctx.data().db, &user, song, chart, i, Some(&author))
.await?;
embeds.push(embed); embeds.push(embed);
attachments.extend(attachment); attachments.extend(attachment);

View file

@ -5,7 +5,6 @@ use poise::{
serenity_prelude::{CreateAttachment, CreateEmbed}, serenity_prelude::{CreateAttachment, CreateEmbed},
CreateReply, CreateReply,
}; };
use sqlx::query;
use crate::{ use crate::{
arcaea::{ arcaea::{
@ -26,7 +25,7 @@ use crate::{
context::{Context, Error}, context::{Context, Error},
get_user, get_user,
logs::debug_image_log, logs::debug_image_log,
reply_errors, reply_errors, timed,
user::User, user::User,
}; };
@ -53,20 +52,20 @@ async fn best_plays(
let user_ctx = ctx.data(); let user_ctx = ctx.data();
let plays = reply_errors!( let plays = reply_errors!(
ctx, ctx,
get_best_plays( timed!("get_best_plays", {
&user_ctx.db, get_best_plays(
&user_ctx.song_cache, user_ctx,
user.id, user.id,
scoring_system, scoring_system,
if require_full { if require_full {
grid_size.0 * grid_size.1 grid_size.0 * grid_size.1
} else { } else {
grid_size.0 * (grid_size.1.max(1) - 1) + 1 grid_size.0 * (grid_size.1.max(1) - 1) + 1
} as usize, } as usize,
(grid_size.0 * grid_size.1) as usize, (grid_size.0 * grid_size.1) as usize,
None None,
) )?
.await? })
); );
// {{{ Layout // {{{ Layout
@ -463,48 +462,42 @@ pub async fn bany(
#[poise::command(prefix_command, slash_command, user_cooldown = 1)] #[poise::command(prefix_command, slash_command, user_cooldown = 1)]
async fn meta(ctx: Context<'_>) -> Result<(), Error> { async fn meta(ctx: Context<'_>) -> Result<(), Error> {
let user = get_user!(&ctx); let user = get_user!(&ctx);
let song_count = query!("SELECT count() as count FROM songs") let conn = ctx.data().db.get()?;
.fetch_one(&ctx.data().db) let song_count: usize = conn
.await? .prepare_cached("SELECT count() as count FROM songs")?
.count; .query_row((), |row| row.get(0))?;
let chart_count = query!("SELECT count() as count FROM charts") let chart_count: usize = conn
.fetch_one(&ctx.data().db) .prepare_cached("SELECT count() as count FROM charts")?
.await? .query_row((), |row| row.get(0))?;
.count;
let users_count = query!("SELECT count() as count FROM users") let users_count: usize = conn
.fetch_one(&ctx.data().db) .prepare_cached("SELECT count() as count FROM users")?
.await? .query_row((), |row| row.get(0))?;
.count;
let pookie_count = query!( let pookie_count: usize = conn
" .prepare_cached(
SELECT count() as count "
FROM users SELECT count() as count
WHERE is_pookie=1 FROM users
" WHERE is_pookie=1
) ",
.fetch_one(&ctx.data().db) )?
.await? .query_row((), |row| row.get(0))?;
.count;
let play_count = query!("SELECT count() as count FROM plays") let play_count: usize = conn
.fetch_one(&ctx.data().db) .prepare_cached("SELECT count() as count FROM plays")?
.await? .query_row((), |row| row.get(0))?;
.count;
let your_play_count = query!( let your_play_count: usize = conn
" .prepare_cached(
"
SELECT count() as count SELECT count() as count
FROM plays FROM plays
WHERE user_id=? WHERE user_id=?
", ",
user.id )?
) .query_row([user.id], |row| row.get(0))?;
.fetch_one(&ctx.data().db)
.await?
.count;
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.title("Bot statistics") .title("Bot statistics")

View file

@ -14,7 +14,7 @@ macro_rules! edit_reply {
#[macro_export] #[macro_export]
macro_rules! get_user { macro_rules! get_user {
($ctx:expr) => {{ ($ctx:expr) => {{
crate::reply_errors!($ctx, crate::user::User::from_context($ctx).await) crate::reply_errors!($ctx, crate::user::User::from_context($ctx))
}}; }};
} }

View file

@ -1,7 +1,6 @@
use r2d2_sqlite::SqliteConnectionManager;
use std::fs; use std::fs;
use sqlx::SqlitePool;
use crate::{ use crate::{
arcaea::{chart::SongCache, jacket::JacketCache}, arcaea::{chart::SongCache, jacket::JacketCache},
assets::{get_data_dir, EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT}, assets::{get_data_dir, EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT},
@ -13,9 +12,11 @@ use crate::{
pub type Error = Box<dyn std::error::Error + Send + Sync>; pub type Error = Box<dyn std::error::Error + Send + Sync>;
pub type Context<'a> = poise::Context<'a, UserContext, Error>; pub type Context<'a> = poise::Context<'a, UserContext, Error>;
pub type DbConnection = r2d2::Pool<SqliteConnectionManager>;
// Custom user data passed to all command functions // Custom user data passed to all command functions
pub struct UserContext { pub struct UserContext {
pub db: SqlitePool, pub db: DbConnection,
pub song_cache: SongCache, pub song_cache: SongCache,
pub jacket_cache: JacketCache, pub jacket_cache: JacketCache,
pub ui_measurements: UIMeasurements, pub ui_measurements: UIMeasurements,
@ -29,11 +30,11 @@ pub struct UserContext {
impl UserContext { impl UserContext {
#[inline] #[inline]
pub async fn new(db: SqlitePool) -> Result<Self, Error> { pub async fn new(db: DbConnection) -> Result<Self, Error> {
timed!("create_context", { timed!("create_context", {
fs::create_dir_all(get_data_dir())?; 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 jacket_cache = timed!("make_jacket_cache", { JacketCache::new(&mut song_cache)? });
let ui_measurements = timed!("read_ui_measurements", { UIMeasurements::read()? }); let ui_measurements = timed!("read_ui_measurements", { UIMeasurements::read()? });

View file

@ -22,9 +22,16 @@ mod user;
use arcaea::play::generate_missing_scores; use arcaea::play::generate_missing_scores;
use assets::get_data_dir; use assets::get_data_dir;
use context::{Error, UserContext}; use context::{Error, UserContext};
use include_dir::{include_dir, Dir};
use poise::serenity_prelude::{self as serenity}; use poise::serenity_prelude::{self as serenity};
use sqlx::sqlite::SqlitePoolOptions; use r2d2::Pool;
use std::{env::var, sync::Arc, time::Duration}; use r2d2_sqlite::SqliteConnectionManager;
use rusqlite_migration::Migrations;
use std::{
env::var,
sync::{Arc, LazyLock},
time::Duration,
};
// {{{ Error handler // {{{ Error handler
async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) { async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) {
@ -40,13 +47,27 @@ async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let pool = SqlitePoolOptions::new() let pool = Pool::new(
.connect(&format!( SqliteConnectionManager::file(&format!("{}/db.sqlite", get_data_dir().to_str().unwrap()))
"sqlite://{}/db.sqlite", .with_init(|conn| {
get_data_dir().to_str().unwrap() static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
)) static MIGRATIONS: LazyLock<Migrations> = LazyLock::new(|| {
.await timed!("create_migration_structure", {
.unwrap(); 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 // {{{ Poise options
let options = poise::FrameworkOptions { let options = poise::FrameworkOptions {

View file

@ -1,9 +1,9 @@
use std::str::FromStr; use std::str::FromStr;
use poise::serenity_prelude::UserId; 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)] #[derive(Debug, Clone)]
pub struct User { pub struct User {
@ -13,35 +13,44 @@ pub struct User {
} }
impl User { impl User {
pub async fn from_context(ctx: &Context<'_>) -> Result<Self, Error> { #[inline]
let id = ctx.author().id.get().to_string(); fn from_row<'a, 'b>(row: &'a Row<'b>) -> Result<Self, rusqlite::Error> {
let user = sqlx::query!("SELECT * FROM users WHERE discord_id = ?", id) Ok(Self {
.fetch_one(&ctx.data().db) id: row.get("id")?,
.await discord_id: row.get("discord_id")?,
.map_err(|_| "You are not an user in my database, sowwy ^~^")?; is_pookie: row.get("is_pookie")?,
Ok(User {
id: user.id as u32,
discord_id: user.discord_id,
is_pookie: user.is_pookie,
}) })
} }
pub async fn by_id(db: &SqlitePool, id: u32) -> Result<Self, Error> { pub fn from_context(ctx: &Context<'_>) -> Result<Self, Error> {
let user = sqlx::query!("SELECT * FROM users WHERE id = ?", id) let id = ctx.author().id.get().to_string();
.fetch_one(db) let user = ctx
.await?; .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 { Ok(user)
id: user.id as u32, }
discord_id: user.discord_id,
is_pookie: user.is_pookie, pub fn by_id(ctx: &UserContext, id: u32) -> Result<Self, Error> {
}) 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] #[inline]
pub async fn discord_it_to_discord_user( pub async fn discord_id_to_discord_user(
&ctx: &Context<'_>, &ctx: &Context<'_>,
discord_id: &str, discord_id: &str,
) -> Result<poise::serenity_prelude::User, Error> { ) -> Result<poise::serenity_prelude::User, Error> {