Compare commits
3 commits
926f616d89
...
520949c07a
Author | SHA1 | Date | |
---|---|---|---|
520949c07a | |||
b01bf69f97 | |||
7caa5f2a08 |
9 changed files with 648 additions and 163 deletions
128
Cargo.lock
generated
128
Cargo.lock
generated
|
@ -73,9 +73,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.3"
|
version = "0.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
|
checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
|
@ -109,9 +109,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.13.0"
|
version = "3.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
|
@ -121,9 +121,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.4.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
|
@ -349,6 +349,12 @@ version = "2.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
|
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "finl_unicode"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
version = "0.10.14"
|
version = "0.10.14"
|
||||||
|
@ -443,7 +449,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.29",
|
"syn 2.0.36",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -615,9 +621,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.5"
|
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 = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
@ -648,9 +654,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.147"
|
version = "0.2.148"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
|
@ -677,9 +683,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.5"
|
version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
|
checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
|
@ -708,9 +714,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.6.0"
|
version = "2.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c"
|
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
|
@ -817,9 +823,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.32.0"
|
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 = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe"
|
checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -890,19 +896,20 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.7.2"
|
version = "2.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a"
|
checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"ucd-trie",
|
"ucd-trie",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_derive"
|
name = "pest_derive"
|
||||||
version = "2.7.2"
|
version = "2.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "666d00490d4ac815001da55838c500eafb0320019bbaa44444137c48b443a853"
|
checksum = "a2bee7be22ce7918f641a33f08e3f43388c7656772244e2bbb2477f44cc9021a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_generator",
|
"pest_generator",
|
||||||
|
@ -910,22 +917,22 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_generator"
|
name = "pest_generator"
|
||||||
version = "2.7.2"
|
version = "2.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68ca01446f50dbda87c1786af8770d535423fa8a53aec03b8f4e3d7eb10e0929"
|
checksum = "d1511785c5e98d79a05e8a6bc34b4ac2168a0e3e92161862030ad84daa223141"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_meta",
|
"pest_meta",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.29",
|
"syn 2.0.36",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_meta"
|
name = "pest_meta"
|
||||||
version = "2.7.2"
|
version = "2.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48"
|
checksum = "b42f0394d3123e33353ca5e1e89092e533d2cc490389f2bd6131c43c634ebc5f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pest",
|
"pest",
|
||||||
|
@ -949,7 +956,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.29",
|
"syn 2.0.36",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -999,9 +1006,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.66"
|
version = "1.0.67"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
|
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
@ -1116,9 +1123,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.10"
|
version = "0.38.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964"
|
checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"errno",
|
"errno",
|
||||||
|
@ -1144,14 +1151,14 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
|
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.3",
|
"base64 0.21.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.101.4"
|
version = "0.101.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d"
|
checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
|
@ -1196,14 +1203,14 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.29",
|
"syn 2.0.36",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.105"
|
version = "1.0.107"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
|
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
|
@ -1280,9 +1287,9 @@ checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.3"
|
version = "0.5.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
|
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
|
@ -1315,9 +1322,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlformat"
|
name = "sqlformat"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e"
|
checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itertools",
|
"itertools",
|
||||||
"nom",
|
"nom",
|
||||||
|
@ -1451,7 +1458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482"
|
checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64 0.21.3",
|
"base64 0.21.4",
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -1495,7 +1502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e"
|
checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64 0.21.3",
|
"base64 0.21.4",
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"crc",
|
"crc",
|
||||||
|
@ -1555,10 +1562,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stringprep"
|
name = "stringprep"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da"
|
checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"finl_unicode",
|
||||||
"unicode-bidi",
|
"unicode-bidi",
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
@ -1588,9 +1596,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.29"
|
version = "2.0.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
|
checksum = "91e02e55d62894af2a08aca894c6577281f76769ba47c94d5756bec8ac6e7373"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1612,22 +1620,22 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.47"
|
version = "1.0.48"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
|
checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.47"
|
version = "1.0.48"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
|
checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.29",
|
"syn 2.0.36",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1720,7 +1728,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.29",
|
"syn 2.0.36",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1755,7 +1763,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.29",
|
"syn 2.0.36",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1769,9 +1777,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.16.0"
|
version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ucd-trie"
|
name = "ucd-trie"
|
||||||
|
@ -1787,9 +1795,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.11"
|
version = "1.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
|
@ -1878,7 +1886,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.29",
|
"syn 2.0.36",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1900,7 +1908,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.29",
|
"syn 2.0.36",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,54 +5,45 @@ use time::PrimitiveDateTime;
|
||||||
use super::{DatabaseError, PlaylistChange, PlaylistEntry, SrcIdOwned};
|
use super::{DatabaseError, PlaylistChange, PlaylistEntry, SrcIdOwned};
|
||||||
|
|
||||||
/// Change operation stored in the database
|
/// Change operation stored in the database
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "typ")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "typ")]
|
||||||
pub enum ChangeOperation {
|
pub enum ChangeOperation {
|
||||||
/// Insert
|
/// Insert
|
||||||
Ins { pos: u32, val: Vec<SrcIdOwned> },
|
Ins { pos: usize, val: Vec<SrcIdOwned> },
|
||||||
/// Delete
|
/// Delete
|
||||||
Del { pos: u32, n: u32 },
|
Del { pos: usize, n: usize },
|
||||||
/// Move
|
/// Move
|
||||||
Mov { pos: u32, n: u32, to: u32 },
|
Mov { pos: usize, n: usize, to: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChangeOperation {
|
impl ChangeOperation {
|
||||||
pub fn to_op(&self, dt: PrimitiveDateTime) -> Result<Operation<PlaylistEntry>, DatabaseError> {
|
pub fn into_op(self, dt: PrimitiveDateTime) -> Result<Operation<PlaylistEntry>, DatabaseError> {
|
||||||
match self {
|
match self {
|
||||||
ChangeOperation::Ins { pos, val } => Ok(Operation::Ins {
|
ChangeOperation::Ins { pos, val } => Ok(Operation::Ins {
|
||||||
pos: (*pos).try_into()?,
|
pos,
|
||||||
val: val
|
val: val.into_iter().map(|id| PlaylistEntry { id, dt }).collect(),
|
||||||
.iter()
|
|
||||||
.map(|id| PlaylistEntry {
|
|
||||||
id: id.to_owned(),
|
|
||||||
dt,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
}),
|
|
||||||
ChangeOperation::Del { pos, n } => Ok(Operation::Del {
|
|
||||||
pos: (*pos).try_into()?,
|
|
||||||
n: (*n).try_into()?,
|
|
||||||
}),
|
}),
|
||||||
|
ChangeOperation::Del { pos, n } => Ok(Operation::Del { pos, n }),
|
||||||
ChangeOperation::Mov { pos, n, to } => {
|
ChangeOperation::Mov { pos, n, to } => {
|
||||||
if *n == 1 {
|
if n == 1 {
|
||||||
Ok(Operation::Mov {
|
Ok(Operation::Mov { pos, to })
|
||||||
pos: (*pos).try_into()?,
|
|
||||||
to: (*to).try_into()?,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
/*
|
let end = pos + n;
|
||||||
let start = usize::try_from(pos)?;
|
|
||||||
let end = usize::try_from(pos + n)?;
|
|
||||||
let to = usize::try_from(to)?;
|
|
||||||
Ok(Operation::Seq {
|
Ok(Operation::Seq {
|
||||||
ops: (start..end)
|
ops: if to > pos {
|
||||||
.rev()
|
// Move down
|
||||||
.enumerate()
|
(pos..end)
|
||||||
.map(|(i, pos)| Operation::Mov { pos, to })
|
.map(|pos| Operation::Mov { pos, to: to + n })
|
||||||
.collect(),
|
.collect()
|
||||||
|
} else {
|
||||||
|
// Move up
|
||||||
|
(pos..end)
|
||||||
|
.rev()
|
||||||
|
.map(|pos| Operation::Mov { pos, to })
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
})
|
})
|
||||||
*/
|
|
||||||
todo!("figure out moving ranges")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +55,9 @@ impl ChangeOperation {
|
||||||
let changes = changes.into_iter();
|
let changes = changes.into_iter();
|
||||||
let mut ops = Vec::with_capacity(changes.size_hint().0);
|
let mut ops = Vec::with_capacity(changes.size_hint().0);
|
||||||
for c in changes {
|
for c in changes {
|
||||||
ops.append(&mut c.operations()?);
|
for cop in &c.operations.0 {
|
||||||
|
ops.push(cop.clone().into_op(c.created_at)?);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(Operation::Seq { ops })
|
Ok(Operation::Seq { ops })
|
||||||
}
|
}
|
||||||
|
@ -76,18 +69,11 @@ impl ChangeOperation {
|
||||||
let changeop = match op {
|
let changeop = match op {
|
||||||
Operation::Nop => return Ok(()),
|
Operation::Nop => return Ok(()),
|
||||||
Operation::Ins { pos, val } => ChangeOperation::Ins {
|
Operation::Ins { pos, val } => ChangeOperation::Ins {
|
||||||
pos: pos.try_into()?,
|
pos,
|
||||||
val: val.into_iter().map(|itm| itm.id).collect(),
|
val: val.into_iter().map(|itm| itm.id).collect(),
|
||||||
},
|
},
|
||||||
Operation::Del { pos, n } => ChangeOperation::Del {
|
Operation::Del { pos, n } => ChangeOperation::Del { pos, n },
|
||||||
pos: pos.try_into()?,
|
Operation::Mov { pos, to } => ChangeOperation::Mov { pos, n: 1, to },
|
||||||
n: n.try_into()?,
|
|
||||||
},
|
|
||||||
Operation::Mov { pos, to } => ChangeOperation::Mov {
|
|
||||||
pos: pos.try_into()?,
|
|
||||||
n: 1,
|
|
||||||
to: to.try_into()?,
|
|
||||||
},
|
|
||||||
Operation::Seq { ops } => {
|
Operation::Seq { ops } => {
|
||||||
return ops
|
return ops
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -97,16 +83,84 @@ impl ChangeOperation {
|
||||||
list.push(changeop);
|
list.push(changeop);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn apply(
|
||||||
|
self,
|
||||||
|
items: &mut Vec<PlaylistEntry>,
|
||||||
|
dt: PrimitiveDateTime,
|
||||||
|
) -> Result<(), DatabaseError> {
|
||||||
|
let assert_pos = |pos: usize| {
|
||||||
|
if pos > items.len() {
|
||||||
|
Err(DatabaseError::PlaylistVcs(
|
||||||
|
format!("index {} out of range (len: {})", pos, items.len()).into(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self {
|
||||||
|
ChangeOperation::Ins { pos, val } => {
|
||||||
|
let new_items = val.into_iter().map(|id| PlaylistEntry { id, dt });
|
||||||
|
if pos < items.len() {
|
||||||
|
items.splice(pos..pos, new_items);
|
||||||
|
} else {
|
||||||
|
items.extend(new_items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChangeOperation::Del { pos, n } => {
|
||||||
|
let end = pos + n;
|
||||||
|
assert_pos(end)?;
|
||||||
|
items.splice(pos..end, None);
|
||||||
|
}
|
||||||
|
ChangeOperation::Mov { pos, n, to } => {
|
||||||
|
let end = pos + n;
|
||||||
|
assert_pos(end)?;
|
||||||
|
assert_pos(to)?;
|
||||||
|
let moved = items.splice(pos..end, None).collect::<Vec<_>>();
|
||||||
|
items.splice(to..to, moved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ChangeOperation {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ChangeOperation::Ins { pos, val } => {
|
||||||
|
f.write_fmt(format_args!("INS {pos}: "))?;
|
||||||
|
let show_max = 5;
|
||||||
|
|
||||||
|
for i in 0..(val.len().min(show_max)) {
|
||||||
|
if i != 0 {
|
||||||
|
f.write_str(", ")?;
|
||||||
|
}
|
||||||
|
val[i].fmt(f)?;
|
||||||
|
}
|
||||||
|
if val.len() > show_max {
|
||||||
|
f.write_fmt(format_args!(" (+{})", val.len() - show_max))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
ChangeOperation::Del { pos, n } => f.write_fmt(format_args!("DEL {pos} ({n})")),
|
||||||
|
ChangeOperation::Mov { pos, n, to } => {
|
||||||
|
f.write_fmt(format_args!("MOV {pos} ({n}) -> {to}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use time::macros::datetime;
|
||||||
|
|
||||||
use crate::models::MusicService;
|
use crate::models::MusicService;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn to_json() {
|
fn to_json() {
|
||||||
let ins = ChangeOperation::Ins {
|
let ins = ChangeOperation::Ins {
|
||||||
pos: 1,
|
pos: 1,
|
||||||
val: vec![SrcIdOwned("Zeerrnuli5E".to_owned(), MusicService::YouTube)],
|
val: vec![SrcIdOwned("Zeerrnuli5E".to_owned(), MusicService::YouTube)],
|
||||||
|
@ -122,14 +176,174 @@ mod tests {
|
||||||
r#"{"typ":"DEL","pos":1,"n":2}"#
|
r#"{"typ":"DEL","pos":1,"n":2}"#
|
||||||
);
|
);
|
||||||
|
|
||||||
let del = ChangeOperation::Mov {
|
let mov = ChangeOperation::Mov {
|
||||||
pos: 1,
|
pos: 1,
|
||||||
n: 2,
|
n: 2,
|
||||||
to: 3,
|
to: 3,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
serde_json::to_string(&del).unwrap(),
|
serde_json::to_string(&mov).unwrap(),
|
||||||
r#"{"typ":"MOV","pos":1,"n":2,"to":3}"#
|
r#"{"typ":"MOV","pos":1,"n":2,"to":3}"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_string() {
|
||||||
|
let ins = ChangeOperation::Ins {
|
||||||
|
pos: 1,
|
||||||
|
val: vec![SrcIdOwned("Zeerrnuli5E".to_owned(), MusicService::YouTube)],
|
||||||
|
};
|
||||||
|
assert_eq!(ins.to_string(), r#"INS 1: yt:Zeerrnuli5E"#);
|
||||||
|
|
||||||
|
let del = ChangeOperation::Del { pos: 1, n: 2 };
|
||||||
|
assert_eq!(del.to_string(), r#"DEL 1 (2)"#);
|
||||||
|
|
||||||
|
let mov = ChangeOperation::Mov {
|
||||||
|
pos: 1,
|
||||||
|
n: 2,
|
||||||
|
to: 3,
|
||||||
|
};
|
||||||
|
assert_eq!(mov.to_string(), r#"MOV 1 (2) -> 3"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TESTDATE: time::PrimitiveDateTime = datetime!(2023-09-06 8:00:00);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_ins() {
|
||||||
|
let mut entries = vec![
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("1".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("4".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let ins = ChangeOperation::Ins {
|
||||||
|
pos: 1,
|
||||||
|
val: vec![
|
||||||
|
SrcIdOwned("2".to_owned(), MusicService::Tiraya),
|
||||||
|
SrcIdOwned("3".to_owned(), MusicService::Tiraya),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
ins.apply(&mut entries, TESTDATE).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(entries.len(), 4);
|
||||||
|
for (i, e) in entries.iter().enumerate() {
|
||||||
|
assert_eq!(e.id.0, (i + 1).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_del() {
|
||||||
|
let mut entries = vec![
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("1".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("2".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("X".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("X".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let del = ChangeOperation::Del { pos: 2, n: 2 };
|
||||||
|
del.apply(&mut entries, TESTDATE).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(entries.len(), 2);
|
||||||
|
for (i, e) in entries.iter().enumerate() {
|
||||||
|
assert_eq!(e.id.0, (i + 1).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_mov_up() {
|
||||||
|
let mut entries = vec![
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("3".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("4".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("1".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("2".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mov = ChangeOperation::Mov {
|
||||||
|
pos: 2,
|
||||||
|
n: 2,
|
||||||
|
to: 0,
|
||||||
|
};
|
||||||
|
mov.apply(&mut entries, TESTDATE).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(entries.len(), 4);
|
||||||
|
for (i, e) in entries.iter().enumerate() {
|
||||||
|
assert_eq!(e.id.0, (i + 1).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_mov_down() {
|
||||||
|
let mut entries = vec![
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("3".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("4".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("1".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("2".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mov = ChangeOperation::Mov {
|
||||||
|
pos: 0,
|
||||||
|
n: 2,
|
||||||
|
to: 2,
|
||||||
|
};
|
||||||
|
mov.apply(&mut entries, TESTDATE).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(entries.len(), 4);
|
||||||
|
for (i, e) in entries.iter().enumerate() {
|
||||||
|
assert_eq!(e.id.0, (i + 1).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_del_out_of_range() {
|
||||||
|
let mut entries = vec![
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("1".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
PlaylistEntry {
|
||||||
|
id: SrcIdOwned("2".to_owned(), MusicService::Tiraya),
|
||||||
|
dt: TESTDATE,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let del = ChangeOperation::Del { pos: 1, n: 2 };
|
||||||
|
let err = del.apply(&mut entries, TESTDATE).unwrap_err();
|
||||||
|
assert!(matches!(err, DatabaseError::PlaylistVcs(_)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,18 +35,14 @@ pub enum DatabaseError {
|
||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
#[error("Item {0} not found")]
|
#[error("Item {0} not found")]
|
||||||
NotFound(IdOwned),
|
NotFound(IdOwned),
|
||||||
|
#[error("Conflict while inserting {typ}: ID {id}")]
|
||||||
|
Conflict { typ: &'static str, id: String },
|
||||||
#[error("Playlist VCS error: {0}")]
|
#[error("Playlist VCS error: {0}")]
|
||||||
PlaylistVcs(Cow<'static, str>),
|
PlaylistVcs(Cow<'static, str>),
|
||||||
#[error("DB error: {0}")]
|
#[error("DB error: {0}")]
|
||||||
Other(Cow<'static, str>),
|
Other(Cow<'static, str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::num::TryFromIntError> for DatabaseError {
|
|
||||||
fn from(value: std::num::TryFromIntError) -> Self {
|
|
||||||
Self::Other(value.to_string().into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum Id<'a> {
|
pub enum Id<'a> {
|
||||||
Db(i32),
|
Db(i32),
|
||||||
|
|
|
@ -28,11 +28,10 @@ pub struct Playlist {
|
||||||
pub updated_at: PrimitiveDateTime,
|
pub updated_at: PrimitiveDateTime,
|
||||||
pub last_sync_at: Option<PrimitiveDateTime>,
|
pub last_sync_at: Option<PrimitiveDateTime>,
|
||||||
pub last_sync_data: Option<Json<SyncData>>,
|
pub last_sync_data: Option<Json<SyncData>>,
|
||||||
pub cache_version: Option<Uuid>,
|
|
||||||
pub cache_data: Option<Json<Vec<PlaylistEntry>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data for creating a playlist
|
/// Data for creating a playlist
|
||||||
|
#[derive(Default)]
|
||||||
pub struct PlaylistNew {
|
pub struct PlaylistNew {
|
||||||
pub src_id: Option<String>,
|
pub src_id: Option<String>,
|
||||||
pub service: Option<MusicService>,
|
pub service: Option<MusicService>,
|
||||||
|
@ -110,8 +109,7 @@ impl Playlist {
|
||||||
Self,
|
Self,
|
||||||
r#"select id, src_id, service as "service: _", name, description,
|
r#"select id, src_id, service as "service: _", name, description,
|
||||||
owner_name, owner_url, playlist_type as "playlist_type: _", image_url, image_hash,
|
owner_name, owner_url, playlist_type as "playlist_type: _", image_url, image_hash,
|
||||||
image_type as "image_type: _", created_at, updated_at, last_sync_at, last_sync_data as "last_sync_data: _",
|
image_type as "image_type: _", created_at, updated_at, last_sync_at, last_sync_data as "last_sync_data: _"
|
||||||
cache_version, cache_data as "cache_data: _"
|
|
||||||
from playlists where id=$1"#,
|
from playlists where id=$1"#,
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
|
@ -123,8 +121,7 @@ from playlists where id=$1"#,
|
||||||
Self,
|
Self,
|
||||||
r#"select id, src_id, service as "service: _", name, description,
|
r#"select id, src_id, service as "service: _", name, description,
|
||||||
owner_name, owner_url, playlist_type as "playlist_type: _", image_url, image_hash,
|
owner_name, owner_url, playlist_type as "playlist_type: _", image_url, image_hash,
|
||||||
image_type as "image_type: _", created_at, updated_at, last_sync_at, last_sync_data as "last_sync_data: _",
|
image_type as "image_type: _", created_at, updated_at, last_sync_at, last_sync_data as "last_sync_data: _"
|
||||||
cache_version, cache_data as "cache_data: _"
|
|
||||||
from playlists where src_id=$1 and service=$2"#,
|
from playlists where src_id=$1 and service=$2"#,
|
||||||
src_id,
|
src_id,
|
||||||
srv as MusicService
|
srv as MusicService
|
||||||
|
@ -210,6 +207,23 @@ from playlists where src_id=$1 and service=$2"#,
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the cached playlist entries for the given version if available
|
||||||
|
async fn get_cache<'a, E>(
|
||||||
|
id: i32,
|
||||||
|
exec: E,
|
||||||
|
) -> Result<Option<(Uuid, Vec<PlaylistEntry>)>, DatabaseError>
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
|
{
|
||||||
|
let res = sqlx::query!(
|
||||||
|
r#"select cache_version, cache_data as "cache_data: Json<Vec<PlaylistEntry>>" from playlists where id=$1"#,
|
||||||
|
id,
|
||||||
|
).fetch_optional(exec).await?;
|
||||||
|
|
||||||
|
Ok(res.and_then(|res| res.cache_version.zip(res.cache_data.map(|d| d.0))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the cached playlist entries
|
||||||
async fn set_cache<'a, E>(
|
async fn set_cache<'a, E>(
|
||||||
id: i32,
|
id: i32,
|
||||||
cache_version: Uuid,
|
cache_version: Uuid,
|
||||||
|
@ -289,17 +303,17 @@ order by pc.created_at"#,
|
||||||
/// Get a list of playlist entries from the current revision
|
/// Get a list of playlist entries from the current revision
|
||||||
///
|
///
|
||||||
/// Merges heads and updates the cache if necessary.
|
/// Merges heads and updates the cache if necessary.
|
||||||
pub async fn get_entries<'a, E>(&self, exec: E) -> Result<Vec<PlaylistEntry>, DatabaseError>
|
pub async fn get_entries<'a, E>(id: i32, exec: E) -> Result<Vec<PlaylistEntry>, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
if let Some(head) = Self::get_single_head(self.id, exec).await? {
|
if let Some(head) = Self::get_single_head(id, exec).await? {
|
||||||
// Get list of items and changes to apply
|
// Get list of items and changes to apply
|
||||||
let (mut items, changes) = if let (Some(cache_version), Some(Json(cache_data))) =
|
let cache_data = Self::get_cache(id, exec).await?;
|
||||||
(self.cache_version, self.cache_data.clone())
|
|
||||||
{
|
let (mut items, changes) = if let Some((cache_version, entries)) = cache_data {
|
||||||
(
|
(
|
||||||
cache_data,
|
entries,
|
||||||
if head != cache_version {
|
if head != cache_version {
|
||||||
PlaylistChange::get_changes(Some(cache_version), head, exec).await?
|
PlaylistChange::get_changes(Some(cache_version), head, exec).await?
|
||||||
} else {
|
} else {
|
||||||
|
@ -315,12 +329,12 @@ order by pc.created_at"#,
|
||||||
|
|
||||||
if !changes.is_empty() {
|
if !changes.is_empty() {
|
||||||
// Apply changes
|
// Apply changes
|
||||||
for c in changes.iter().rev() {
|
for c in changes.into_iter().rev() {
|
||||||
c.apply(&mut items)?;
|
c.apply(&mut items)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
Self::set_cache(self.id, head, &items, exec).await?;
|
Self::set_cache(id, head, &items, exec).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(items)
|
Ok(items)
|
||||||
|
@ -340,18 +354,20 @@ order by pc.created_at"#,
|
||||||
let changes = PlaylistChange::get_changes(None, head, exec).await?;
|
let changes = PlaylistChange::get_changes(None, head, exec).await?;
|
||||||
|
|
||||||
// Apply changes
|
// Apply changes
|
||||||
for c in changes.iter().rev() {
|
for c in changes.into_iter().rev() {
|
||||||
c.apply(&mut items)?;
|
c.apply(&mut items)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_tracks<'a, E>(&self, exec: E) -> Result<Vec<PlaylistEntryTrack>, DatabaseError>
|
pub async fn get_tracks<'a, E>(
|
||||||
|
id: i32,
|
||||||
|
exec: E,
|
||||||
|
) -> Result<Vec<PlaylistEntryTrack>, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
let entries = self.get_entries(exec).await?;
|
let entries = Self::get_entries(id, exec).await?;
|
||||||
Self::tracks_from_entries(&entries, exec).await
|
Self::tracks_from_entries(&entries, exec).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -765,14 +781,18 @@ mod tests {
|
||||||
async fn get_tracks() {
|
async fn get_tracks() {
|
||||||
testutil::run_sql("base.sql", &pool).await;
|
testutil::run_sql("base.sql", &pool).await;
|
||||||
|
|
||||||
let pl = Playlist::get(ids::PLAYLIST_TEST1, &pool).await.unwrap();
|
let tracks = Playlist::get_tracks(ids::PLAYLIST_ID_TEST1, &pool)
|
||||||
let tracks = pl.get_tracks(&pool).await.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
insta::assert_ron_snapshot!("get_tracks", tracks);
|
insta::assert_ron_snapshot!("get_tracks", tracks);
|
||||||
|
|
||||||
// Check cache
|
// Check cache
|
||||||
let pl = Playlist::get(ids::PLAYLIST_TEST1, &pool).await.unwrap();
|
let (cache_version, cache_entries) = Playlist::get_cache(ids::PLAYLIST_ID_TEST1, &pool)
|
||||||
assert_eq!(pl.cache_version.unwrap(), ids::PLAYLIST_CHANGE_HEAD);
|
.await
|
||||||
for (i, entry) in pl.cache_data.unwrap().0.iter().enumerate() {
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cache_version, ids::PLAYLIST_CHANGE_HEAD);
|
||||||
|
for (i, entry) in cache_entries.iter().enumerate() {
|
||||||
let t = &tracks[i];
|
let t = &tracks[i];
|
||||||
let track = t.track.as_ref().unwrap();
|
let track = t.track.as_ref().unwrap();
|
||||||
assert_eq!(t.n, u32::try_from(i).unwrap());
|
assert_eq!(t.n, u32::try_from(i).unwrap());
|
||||||
|
@ -819,9 +839,9 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
tx.commit().await.unwrap();
|
tx.commit().await.unwrap();
|
||||||
|
|
||||||
let pl = Playlist::get(ids::PLAYLIST_TEST1, &pool).await.unwrap();
|
let entries = Playlist::get_entries(ids::PLAYLIST_ID_TEST1, &pool)
|
||||||
let entries = pl.get_entries(&pool).await.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
testutil::compare_pl_entries(
|
testutil::compare_pl_entries(
|
||||||
&entries,
|
&entries,
|
||||||
|
@ -829,10 +849,104 @@ mod tests {
|
||||||
SrcId("WSBUeFdXiSs", MusicService::YouTube),
|
SrcId("WSBUeFdXiSs", MusicService::YouTube),
|
||||||
SrcId("OCgE2GSL1Pk", MusicService::YouTube),
|
SrcId("OCgE2GSL1Pk", MusicService::YouTube),
|
||||||
SrcId("6485PhOtHzY", MusicService::YouTube),
|
SrcId("6485PhOtHzY", MusicService::YouTube),
|
||||||
SrcId("2txScm52-QI", MusicService::YouTube)
|
SrcId("2txScm52-QI", MusicService::YouTube),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
"got: {entries:#?}"
|
"got: {entries:#?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx_database_tester::test(pool(variable = "pool"))]
|
||||||
|
async fn add_changes_1() {
|
||||||
|
testutil::run_sql("base.sql", &pool).await;
|
||||||
|
|
||||||
|
let mut tx = pool.begin().await.unwrap();
|
||||||
|
Playlist::add_change(
|
||||||
|
Some(ids::PLAYLIST_CHANGE_HEAD),
|
||||||
|
ids::PLAYLIST_ID_TEST1,
|
||||||
|
vec![ChangeOperation::Ins {
|
||||||
|
pos: 3,
|
||||||
|
val: vec![SrcIdOwned("2txScm52-QI".to_owned(), MusicService::YouTube)],
|
||||||
|
}],
|
||||||
|
&mut tx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.commit().await.unwrap();
|
||||||
|
|
||||||
|
let mut tx = pool.begin().await.unwrap();
|
||||||
|
Playlist::add_change(
|
||||||
|
Some(ids::PLAYLIST_CHANGE_I3),
|
||||||
|
ids::PLAYLIST_ID_TEST1,
|
||||||
|
vec![ChangeOperation::Del { pos: 1, n: 1 }],
|
||||||
|
&mut tx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.commit().await.unwrap();
|
||||||
|
|
||||||
|
let entries = Playlist::get_entries(ids::PLAYLIST_ID_TEST1, &pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
testutil::compare_pl_entries(
|
||||||
|
&entries,
|
||||||
|
&[
|
||||||
|
SrcId("WSBUeFdXiSs", MusicService::YouTube),
|
||||||
|
SrcId("6485PhOtHzY", MusicService::YouTube),
|
||||||
|
SrcId("2txScm52-QI", MusicService::YouTube),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"got: {entries:#?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a change to the playlist and run the merge operation concurrently to make
|
||||||
|
/// sure it does not create any ghost revisions.
|
||||||
|
#[sqlx_database_tester::test(pool(variable = "pool"))]
|
||||||
|
async fn merge_concurrent() {
|
||||||
|
testutil::run_sql("base.sql", &pool).await;
|
||||||
|
|
||||||
|
let mut tx = pool.begin().await.unwrap();
|
||||||
|
Playlist::add_change(
|
||||||
|
Some(ids::PLAYLIST_CHANGE_I3),
|
||||||
|
ids::PLAYLIST_ID_TEST1,
|
||||||
|
vec![ChangeOperation::Ins {
|
||||||
|
pos: 3,
|
||||||
|
val: vec![SrcIdOwned("2txScm52-QI".to_owned(), MusicService::YouTube)],
|
||||||
|
}],
|
||||||
|
&mut tx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.commit().await.unwrap();
|
||||||
|
|
||||||
|
stream::iter(0..8)
|
||||||
|
.for_each_concurrent(8, |_| {
|
||||||
|
let pool = pool.clone();
|
||||||
|
async move {
|
||||||
|
let entries = Playlist::get_entries(ids::PLAYLIST_ID_TEST1, &pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
testutil::compare_pl_entries(
|
||||||
|
&entries,
|
||||||
|
&[
|
||||||
|
SrcId("WSBUeFdXiSs", MusicService::YouTube),
|
||||||
|
SrcId("OCgE2GSL1Pk", MusicService::YouTube),
|
||||||
|
SrcId("6485PhOtHzY", MusicService::YouTube),
|
||||||
|
SrcId("2txScm52-QI", MusicService::YouTube),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"got: {entries:#?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let revs = PlaylistChange::get_all_changes(ids::PLAYLIST_ID_TEST1, &pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(revs.len(), 12);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use nonempty_collections::{nev, NonEmptyIterator};
|
use nonempty_collections::{nev, NonEmptyIterator};
|
||||||
use otvec::Operation;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::types::Json;
|
use sqlx::types::Json;
|
||||||
use time::PrimitiveDateTime;
|
use time::PrimitiveDateTime;
|
||||||
|
@ -115,16 +116,9 @@ parent1_id, parent2_id, created_at from playlist_changes where playlist_id=$1 or
|
||||||
).fetch_all(exec).await
|
).fetch_all(exec).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn operations(&self) -> Result<Vec<Operation<PlaylistEntry>>, DatabaseError> {
|
pub fn apply(self, items: &mut Vec<PlaylistEntry>) -> Result<(), DatabaseError> {
|
||||||
self.operations
|
for op in self.operations.0 {
|
||||||
.iter()
|
op.apply(items, self.created_at)?;
|
||||||
.map(|op| op.to_op(self.created_at))
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply(&self, items: &mut Vec<PlaylistEntry>) -> Result<(), DatabaseError> {
|
|
||||||
for op in &self.operations.0 {
|
|
||||||
op.to_op(self.created_at)?.apply(items);
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -273,6 +267,54 @@ parent1_id, parent2_id, created_at from playlist_changes where playlist_id=$1 or
|
||||||
ncm.insert_with_id(merge_ids.merge, exec).await?;
|
ncm.insert_with_id(merge_ids.merge, exec).await?;
|
||||||
Ok(merge_ids.merge)
|
Ok(merge_ids.merge)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a Graphviz graph from playlist changes
|
||||||
|
pub fn change_graph(changes: &[PlaylistChange]) -> String {
|
||||||
|
let mut g = String::from("digraph {\n node [shape=box];\n");
|
||||||
|
|
||||||
|
let line_start = " \"";
|
||||||
|
let line_end = "\"\n";
|
||||||
|
let arrow = "\"->\"";
|
||||||
|
|
||||||
|
for change in changes {
|
||||||
|
let cid = change.id.to_string();
|
||||||
|
|
||||||
|
g += line_start;
|
||||||
|
g += &cid;
|
||||||
|
g += "\" [label=<<b>#";
|
||||||
|
g += &change.seq.to_string();
|
||||||
|
g += "</b><br/>";
|
||||||
|
g += &cid;
|
||||||
|
g += "<br/><font point-size=\"10\">";
|
||||||
|
|
||||||
|
for op in change.operations.0.iter().take(5) {
|
||||||
|
g += "<br/>";
|
||||||
|
g += &op.to_string();
|
||||||
|
}
|
||||||
|
if change.operations.0.len() > 5 {
|
||||||
|
g += &format!("<br>({} more ops)", change.operations.0.len() - 5);
|
||||||
|
}
|
||||||
|
g += " </font>>]\n";
|
||||||
|
|
||||||
|
if let Some(p1) = change.parent1_id {
|
||||||
|
g += line_start;
|
||||||
|
g += &p1.to_string();
|
||||||
|
g += arrow;
|
||||||
|
g += &cid;
|
||||||
|
g += line_end;
|
||||||
|
}
|
||||||
|
if let Some(p2) = change.parent2_id {
|
||||||
|
g += line_start;
|
||||||
|
g += &p2.to_string();
|
||||||
|
g += arrow;
|
||||||
|
g += &cid;
|
||||||
|
g += line_end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g += "}\n";
|
||||||
|
g
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlaylistChangeNew {
|
impl PlaylistChangeNew {
|
||||||
|
@ -297,12 +339,11 @@ values ($1,$2,$3,$4,$5,$6) returning id"#,
|
||||||
|
|
||||||
pub async fn insert_with_id<'a, 'b, E>(&'b self, id: Uuid, exec: E) -> Result<(), DatabaseError>
|
pub async fn insert_with_id<'a, 'b, E>(&'b self, id: Uuid, exec: E) -> Result<(), DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
sqlx::query!(
|
let res = sqlx::query!(
|
||||||
r#"insert into playlist_changes (id,seq,operations,playlist_id,parent1_id,parent2_id,created_at)
|
r#"insert into playlist_changes (id,seq,operations,playlist_id,parent1_id,parent2_id,created_at)
|
||||||
values ($1,$2,$3,$4,$5,$6,$7)
|
values ($1,$2,$3,$4,$5,$6,$7)"#,
|
||||||
on conflict (id) do nothing"#,
|
|
||||||
id,
|
id,
|
||||||
self.seq,
|
self.seq,
|
||||||
serde_json::to_value(&self.operations)?,
|
serde_json::to_value(&self.operations)?,
|
||||||
|
@ -310,16 +351,42 @@ on conflict (id) do nothing"#,
|
||||||
self.parent1_id,
|
self.parent1_id,
|
||||||
self.parent2_id,
|
self.parent2_id,
|
||||||
self.created_at.unwrap_or_else(|| util::primitive_now()),
|
self.created_at.unwrap_or_else(|| util::primitive_now()),
|
||||||
).execute(exec).await?;
|
).execute(exec).await;
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
match &e {
|
||||||
|
sqlx::Error::Database(dbe) => {
|
||||||
|
if dbe.is_unique_violation() {
|
||||||
|
let conflicting = PlaylistChange::get(id, exec).await?;
|
||||||
|
if self.seq != conflicting.seq
|
||||||
|
|| self.playlist_id != conflicting.playlist_id
|
||||||
|
|| self.operations != conflicting.operations.0
|
||||||
|
|| self.parent1_id != conflicting.parent1_id
|
||||||
|
|| self.parent2_id != conflicting.parent2_id
|
||||||
|
{
|
||||||
|
return Err(DatabaseError::Conflict {
|
||||||
|
typ: "playlist_change",
|
||||||
|
id: id.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(DatabaseError::Database(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Err(DatabaseError::Database(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use time::macros::datetime;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
models::Playlist,
|
models::{MusicService, Playlist, SrcIdOwned},
|
||||||
testutil::{self, ids},
|
testutil::{self, ids},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -353,6 +420,81 @@ mod tests {
|
||||||
insta::assert_ron_snapshot!(res);
|
insta::assert_ron_snapshot!(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx_database_tester::test(pool(variable = "pool"))]
|
||||||
|
async fn insert_change() {
|
||||||
|
testutil::run_sql("base.sql", &pool).await;
|
||||||
|
|
||||||
|
let nc = PlaylistChangeNew {
|
||||||
|
seq: 7,
|
||||||
|
playlist_id: ids::PLAYLIST_ID_TEST1,
|
||||||
|
operations: vec![ChangeOperation::Ins {
|
||||||
|
pos: 1,
|
||||||
|
val: vec![SrcIdOwned("abcd".to_string(), MusicService::Tiraya)],
|
||||||
|
}],
|
||||||
|
parent1_id: Some(ids::PLAYLIST_CHANGE_HEAD),
|
||||||
|
parent2_id: None,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let id = nc.insert(&pool).await.unwrap();
|
||||||
|
|
||||||
|
let got = PlaylistChange::get(id, &pool).await.unwrap();
|
||||||
|
assert_eq!(got.id, id);
|
||||||
|
assert_eq!(got.seq, nc.seq);
|
||||||
|
assert_eq!(got.operations.0, nc.operations);
|
||||||
|
assert_eq!(got.parent1_id, nc.parent1_id);
|
||||||
|
assert_eq!(got.parent2_id, nc.parent2_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trying to insert changes with same ID and content as already existing ones
|
||||||
|
/// should result in no action
|
||||||
|
#[sqlx_database_tester::test(pool(variable = "pool"))]
|
||||||
|
async fn insert_with_id_duplicate() {
|
||||||
|
testutil::run_sql("base.sql", &pool).await;
|
||||||
|
|
||||||
|
let nc = PlaylistChangeNew {
|
||||||
|
seq: 6,
|
||||||
|
playlist_id: ids::PLAYLIST_ID_TEST1,
|
||||||
|
operations: Vec::new(),
|
||||||
|
parent1_id: Some(ids::PLAYLIST_CHANGE_BR1T),
|
||||||
|
parent2_id: Some(ids::PLAYLIST_CHANGE_BR2T),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
nc.insert_with_id(ids::PLAYLIST_CHANGE_HEAD, &pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let change = PlaylistChange::get(ids::PLAYLIST_CHANGE_HEAD, &pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(change.created_at, datetime!(2023-09-06 23:50:59.397384));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trying to insert changes with same ID and different content as already existing ones
|
||||||
|
/// should result in an error
|
||||||
|
#[sqlx_database_tester::test(pool(variable = "pool"))]
|
||||||
|
async fn insert_with_id_conflict() {
|
||||||
|
testutil::run_sql("base.sql", &pool).await;
|
||||||
|
|
||||||
|
let nc = PlaylistChangeNew {
|
||||||
|
seq: 6,
|
||||||
|
playlist_id: ids::PLAYLIST_ID_TEST1,
|
||||||
|
operations: Vec::new(),
|
||||||
|
parent1_id: Some(ids::PLAYLIST_CHANGE_BR1T),
|
||||||
|
parent2_id: None,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let err = nc
|
||||||
|
.insert_with_id(ids::PLAYLIST_CHANGE_HEAD, &pool)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
if let DatabaseError::Conflict { typ, id } = err {
|
||||||
|
assert_eq!(typ, "playlist_change");
|
||||||
|
assert_eq!(id, ids::PLAYLIST_CHANGE_HEAD.to_string());
|
||||||
|
} else {
|
||||||
|
panic!("unexpected err: {err}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[sqlx_database_tester::test(pool(variable = "pool"))]
|
#[sqlx_database_tester::test(pool(variable = "pool"))]
|
||||||
async fn paths_to_lca() {
|
async fn paths_to_lca() {
|
||||||
testutil::run_sql("base.sql", &pool).await;
|
testutil::run_sql("base.sql", &pool).await;
|
||||||
|
|
|
@ -18,6 +18,4 @@ Playlist(
|
||||||
updated_at: "[date]",
|
updated_at: "[date]",
|
||||||
last_sync_at: None,
|
last_sync_at: None,
|
||||||
last_sync_data: None,
|
last_sync_data: None,
|
||||||
cache_version: None,
|
|
||||||
cache_data: None,
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,6 +18,4 @@ Playlist(
|
||||||
updated_at: "[date]",
|
updated_at: "[date]",
|
||||||
last_sync_at: None,
|
last_sync_at: None,
|
||||||
last_sync_data: None,
|
last_sync_data: None,
|
||||||
cache_version: None,
|
|
||||||
cache_data: None,
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -39,11 +39,12 @@ pub enum DatePrecision {
|
||||||
Year,
|
Year,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
|
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
#[sqlx(type_name = "playlist_type", rename_all = "snake_case")]
|
#[sqlx(type_name = "playlist_type", rename_all = "snake_case")]
|
||||||
pub enum PlaylistType {
|
pub enum PlaylistType {
|
||||||
/// Local playlist created by a Tiraya user
|
/// Local playlist created by a Tiraya user
|
||||||
|
#[default]
|
||||||
Local,
|
Local,
|
||||||
/// Remote playlist hosted by another service
|
/// Remote playlist hosted by another service
|
||||||
Remote,
|
Remote,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
|
|
||||||
use crate::models::{PlaylistEntry, SrcId};
|
use crate::models::{PlaylistChange, PlaylistEntry, SrcId};
|
||||||
|
|
||||||
pub static TESTDATA: Lazy<PathBuf> = Lazy::new(|| path!(env!("CARGO_MANIFEST_DIR") / "testdata"));
|
pub static TESTDATA: Lazy<PathBuf> = Lazy::new(|| path!(env!("CARGO_MANIFEST_DIR") / "testdata"));
|
||||||
|
|
||||||
|
@ -64,3 +64,17 @@ pub fn compare_pl_entries(entries: &[PlaylistEntry], ids: &[SrcId]) -> bool {
|
||||||
}
|
}
|
||||||
entries.iter().zip(ids).all(|(a, b)| a.id == *b)
|
entries.iter().zip(ids).all(|(a, b)| a.id == *b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Print the change graph of a playlist
|
||||||
|
///
|
||||||
|
/// Use Graphviz to visualize: <https://edotor.net/>
|
||||||
|
pub async fn print_change_graph<'a, E>(playlist_id: i32, exec: E)
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
|
{
|
||||||
|
let changes = PlaylistChange::get_all_changes(playlist_id, exec)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let graph = PlaylistChange::change_graph(&changes);
|
||||||
|
println!("{graph}");
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue